├── README.md ├── questions.md └── solutions ├── .gitkeep ├── expense_sharing ├── __init__.py ├── exceptions.py ├── main.py ├── models │ ├── __init__.py │ ├── expenses.py │ └── users.py └── services │ ├── __init__.py │ ├── expenses.py │ └── users.py ├── in_memory_rdms ├── README.md ├── __init__.py ├── constraints.py ├── fields.py └── table.py └── nokia_snake_game └── main.py /README.md: -------------------------------------------------------------------------------- 1 | # Low Level Design Primer Python 2 | 3 | ## Motivation 4 | Learn low level design of system at scale. prepare for the low level design (LLD) / Machine Coding round interviews. 5 | 6 | ## Learn to design low level system 7 | Learning low level design of scalable systems will help you become better engineer. 8 | 9 | This repo is an organized collection of resources to help you learn low level design of systesm's. 10 | 11 | ## Under development 12 | Interested in adding a section or helping complete one in-progress? Contribute! 13 | 14 | - how to guide and study material along with various resources. 15 | - add more questions and improve exisiting questions. 16 | - add solutions for the problems along with their detailed explaination (maybe video) 17 | - create new issues and pick any existing unassigned isssue. 18 | 19 | ## Points to Remember 20 | - Please create a separate directory for each new problem [here](https://github.com/JINDALG/low-level-design-primer-python/tree/main/solutions) 21 | - Please push only working code 22 | - Please mention python version in main file 23 | - please add main.py as an entry-point for each design. 24 | - please add demo if possible. 25 | - Please add Readme.md file for each design to provide meta info like what design pattern have been used, what feature has been Supported and not Supported and etc. 26 | 27 | # Refrences 28 | - [Low Level Design Primer Java](https://github.com/prasadgujar/low-level-design-primer) 29 | 30 | # Contact Info 31 | Feel free to contact me to discuss any issues, questions, or comments. 32 | 33 | My contact info can be found on my [Github](http://github.com/jinDALG/) Page 34 | 35 | # License 36 | TODO 37 | 38 | -------------------------------------------------------------------------------- /questions.md: -------------------------------------------------------------------------------- 1 | # Questions 2 | 3 | Question | Solutions | Videos 4 | | :---: | :-: | :-: 5 | |Design True caller | | | 6 | |Design In-Memory RDMS |[Solution-1](https://github.com/JINDALG/low-level-design-primer-python/tree/main/solutions/in_memory_rdms) | | 7 | |Design SplitWise | [Solution-1](https://github.com/JINDALG/low-level-design-primer-python/tree/main/solutions/expense_sharing)| | 8 | |Design Snakes and Ladder | | | 9 | |Design Cab system(Uber, Ola, Left, Grab | | | 10 | |Design Chess | [Solution 1](https://gist.github.com/rsheldiii/2993225) | | 11 | |Nokia Snake Game | [Solution 1](https://github.com/JINDALG/low-level-design-primer-python/tree/main/solutions/nokia_snake_game) | | 12 | -------------------------------------------------------------------------------- /solutions/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /solutions/expense_sharing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamshivam007/low-level-design-primer-python/1ebd69b8a5bb7258bd7ced002256c7da59e897b4/solutions/expense_sharing/__init__.py -------------------------------------------------------------------------------- /solutions/expense_sharing/exceptions.py: -------------------------------------------------------------------------------- 1 | class InValidAmount(Exception): 2 | message = 'Total amount and user shares are not matching' 3 | 4 | 5 | class UserAlreadyExist(Exception): 6 | message = 'User already exists' 7 | -------------------------------------------------------------------------------- /solutions/expense_sharing/main.py: -------------------------------------------------------------------------------- 1 | # Python3.7 2 | 3 | from expense_sharing.services.users import user_service 4 | from expense_sharing.services.expenses import expense_service 5 | 6 | 7 | user_service.show_expense() 8 | user_service.get_or_add_user('user1').show_expense() 9 | expense_service.add_equal_expense('user1', 1000, ['user1', 'user2', 'user3', 'user4']) 10 | user_service.get_or_add_user('user4').show_expense() 11 | user_service.get_or_add_user('user1').show_expense() 12 | expense_service.add_exact_expense('user1', 1250, ['user2', 'user3'], [370, 880]) 13 | user_service.show_expense() 14 | # user_service.get_or_add_user('user1').show_expense() 15 | # user_service.show_expense() 16 | -------------------------------------------------------------------------------- /solutions/expense_sharing/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamshivam007/low-level-design-primer-python/1ebd69b8a5bb7258bd7ced002256c7da59e897b4/solutions/expense_sharing/models/__init__.py -------------------------------------------------------------------------------- /solutions/expense_sharing/models/expenses.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | 3 | from expense_sharing.models import users as users_models 4 | 5 | 6 | class Expense: 7 | total_amount: int 8 | paid_by: users_models.User 9 | user_shares: Dict[users_models.User, int] 10 | 11 | def __init__(self, paid_by: users_models.User, total_amount: int, user_shares): 12 | self.paid_by = paid_by 13 | self.total_amount = total_amount 14 | self.user_shares = user_shares 15 | 16 | def update_user_calculation(self): 17 | """ 18 | Update each user's user-balance-mapping to update consolidated data 19 | """ 20 | for user, amount in self.user_shares.items(): 21 | user.add_expense(self) 22 | if user != self.paid_by: 23 | # Update paid by user mapping 24 | if user.user_id in self.paid_by.user_balance_mapping: 25 | self.paid_by.user_balance_mapping[user.user_id] += amount 26 | else: 27 | self.paid_by.user_balance_mapping[user.user_id] = amount 28 | 29 | # Update other participants mapping 30 | if self.paid_by.user_id in user.user_balance_mapping: 31 | user.user_balance_mapping[self.paid_by.user_id] -= amount 32 | else: 33 | user.user_balance_mapping[self.paid_by.user_id] = -amount 34 | -------------------------------------------------------------------------------- /solutions/expense_sharing/models/users.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | 4 | class User: 5 | user_id: str 6 | name: str 7 | email: str 8 | user_balance_mapping: Dict[str, int] 9 | expenses = [] 10 | 11 | def __init__(self, user_id: str, name: str, email: str): 12 | self.user_id = user_id 13 | self.name = name 14 | self.email = email 15 | self.user_balance_mapping = {} 16 | self.expenses = [] 17 | 18 | def add_expense(self, expense): 19 | self.expenses.append(expense) 20 | 21 | def show_expense(self): 22 | formatted_output = [] 23 | for user_id, amount in self.user_balance_mapping.items(): 24 | if amount < 0: 25 | formatted_output.append(f'{self.user_id} owes {user_id}: {-amount}') 26 | elif amount > 0: 27 | formatted_output.append(f'{user_id} owes {self.user_id}: {amount}') 28 | if formatted_output: 29 | print('\n'.join(formatted_output)) 30 | else: 31 | print('No balances') 32 | print() 33 | -------------------------------------------------------------------------------- /solutions/expense_sharing/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamshivam007/low-level-design-primer-python/1ebd69b8a5bb7258bd7ced002256c7da59e897b4/solutions/expense_sharing/services/__init__.py -------------------------------------------------------------------------------- /solutions/expense_sharing/services/expenses.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from expense_sharing import exceptions 4 | from expense_sharing.models import expenses as expense_models 5 | from expense_sharing.services.users import user_service 6 | 7 | EXACT = 'EXACT' 8 | EQUAL = 'EQUAL' 9 | 10 | 11 | class ExpenseService: 12 | 13 | @staticmethod 14 | def add_equal_expense(paid_by, total_amount, participants): 15 | users_shares = {} 16 | total_participants = len(participants) 17 | # Calculate share of each user 18 | for index in range(total_participants): 19 | user = user_service.get_or_add_user(participants[index]) 20 | users_shares[user] = total_amount / total_participants 21 | 22 | expense = expense_models.Expense( 23 | user_service.get_or_add_user(paid_by), 24 | total_amount, 25 | users_shares 26 | ) 27 | expense.update_user_calculation() 28 | 29 | @staticmethod 30 | def add_exact_expense(paid_by, total_amount, participants, shares: List[int]): 31 | users_shares = {} 32 | total_participants = len(participants) 33 | 34 | if total_amount == sum(shares): 35 | for index in range(total_participants): 36 | user = user_service.get_or_add_user(participants[index]) 37 | users_shares[user] = shares[index] 38 | else: 39 | raise exceptions.InValidAmount() 40 | 41 | expense = expense_models.Expense( 42 | user_service.get_or_add_user(paid_by), 43 | total_amount, 44 | users_shares 45 | ) 46 | expense.update_user_calculation() 47 | 48 | 49 | expense_service = ExpenseService() 50 | -------------------------------------------------------------------------------- /solutions/expense_sharing/services/users.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from expense_sharing.models import users as users_models 4 | from expense_sharing import exceptions 5 | 6 | 7 | class UserService: 8 | users: Dict[str, users_models.User] 9 | 10 | def __init__(self): 11 | self.users = {} 12 | 13 | def get_user(self, user_id): 14 | return self.users.get(user_id) 15 | 16 | def add_user(self, user_id, name='', email=''): 17 | if user_id in self.users: 18 | raise exceptions.UserAlreadyExist() 19 | user = users_models.User(user_id, name, email) 20 | self.users[user.user_id] = user 21 | return user 22 | 23 | def get_or_add_user(self, user_id, name='', email=''): 24 | user = self.get_user(user_id) 25 | if not user: 26 | user = self.add_user(user_id, name, email) 27 | return user 28 | 29 | def show_expense(self): 30 | formatted_output = [] 31 | for user in self.users.values(): 32 | user_output = [] 33 | for user_id, amount in user.user_balance_mapping.items(): 34 | if amount < 0: 35 | user_output.append(f'{user.user_id} owes {user_id}: {-amount}') 36 | elif amount > 0: 37 | user_output.append(f'{user_id} owes {user.user_id}: {amount}') 38 | if user_output: 39 | formatted_output.append(f'{user.user_id} Expense data') 40 | formatted_output.extend(user_output) 41 | if formatted_output: 42 | print('\n'.join(formatted_output)) 43 | else: 44 | print('No balances') 45 | print() 46 | 47 | 48 | user_service = UserService() 49 | -------------------------------------------------------------------------------- /solutions/in_memory_rdms/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /solutions/in_memory_rdms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamshivam007/low-level-design-primer-python/1ebd69b8a5bb7258bd7ced002256c7da59e897b4/solutions/in_memory_rdms/__init__.py -------------------------------------------------------------------------------- /solutions/in_memory_rdms/constraints.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class Constraint: 5 | 6 | @abc.abstractmethod 7 | def validate(self, value): 8 | pass 9 | 10 | 11 | class PrimaryKey(Constraint): 12 | name = 'primary_key' 13 | is_dml = False 14 | is_ddl = True 15 | 16 | def validate(self, value): 17 | pass 18 | 19 | 20 | class NotNullConstraint(Constraint): 21 | name = 'not_null' 22 | is_dml = True 23 | is_ddl = False 24 | 25 | def validate(self, value): 26 | pass 27 | 28 | @staticmethod 29 | def validate_data(value): 30 | if value is None: 31 | raise ValueError('Null value not allowed.') 32 | -------------------------------------------------------------------------------- /solutions/in_memory_rdms/fields.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class Field: 5 | DATA_TYPE = NotImplemented 6 | 7 | @abc.abstractmethod 8 | def validate(self, *args, **kwargs): 9 | pass 10 | 11 | def validate_data(self, value): 12 | if not isinstance(value, self.DATA_TYPE): 13 | raise TypeError 14 | 15 | 16 | class StringField(Field): 17 | SUPPORTED_MAX_LENGTH = 20 18 | DATA_TYPE = str 19 | 20 | def __init__(self, max_length): 21 | self.max_length = max_length 22 | 23 | def validate(self): 24 | if self.max_length > self.SUPPORTED_MAX_LENGTH: 25 | raise ValueError(f'Supported max length is {self.SUPPORTED_MAX_LENGTH}') 26 | 27 | def validate_data(self, value): 28 | super(StringField, self).validate_data(value) 29 | if len(value) > self.max_length: 30 | raise ValueError('extra length') 31 | 32 | 33 | class IntegerField(Field): 34 | SUPPORTED_MIN_VALUE = -1024 35 | SUPPORTED_MAX_VALUE = 1024 36 | DATA_TYPE = int 37 | 38 | def __init__(self, min_value=None, max_value=None): 39 | self.min_value = self.SUPPORTED_MIN_VALUE if min_value is None else min_value 40 | self.max_value = self.SUPPORTED_MAX_VALUE if max_value is None else max_value 41 | 42 | def validate(self): 43 | if self.min_value > self.max_value: 44 | raise ValueError('min_value should not be greater than max_value') 45 | if self.min_value < self.SUPPORTED_MIN_VALUE: 46 | raise ValueError('min_value error') 47 | if self.max_value > self.SUPPORTED_MAX_VALUE: 48 | raise ValueError('max_value error') 49 | 50 | def validate_data(self, value): 51 | super(IntegerField, self).validate_data(value) 52 | if value and value > self.max_value: 53 | raise ValueError('extra value') 54 | if value and value < self.min_value: 55 | raise ValueError('less value') 56 | -------------------------------------------------------------------------------- /solutions/in_memory_rdms/table.py: -------------------------------------------------------------------------------- 1 | import constraints, fields 2 | 3 | 4 | class Column: 5 | 6 | def __init__(self, name, field_type: fields.Field, column_constraints): 7 | self.field_type = field_type 8 | self.name = name 9 | self.constraints = column_constraints 10 | self.validate() 11 | 12 | def validate(self): 13 | self.field_type.validate() 14 | for constraint in self.constraints: 15 | constraint.is_ddl and constraint.validate() 16 | 17 | def validate_value(self, value): 18 | self.field_type.validate_data(value) 19 | for constraint in self.constraints: 20 | constraint.is_dml and constraint.validate_data(value) 21 | 22 | 23 | class Table: 24 | def __init__(self, name, columns): 25 | self.name = name 26 | self.column_map = {column.name: column for column in columns} 27 | self.rows = [] 28 | 29 | def add_record(self, data_mapping): 30 | if list(data_mapping.keys()) != list(self.column_map.keys()): 31 | raise ValueError('columns mismatch') 32 | 33 | for name, value in data_mapping.items(): 34 | self.column_map[name].validate_value(value) 35 | self.rows.append(data_mapping) 36 | 37 | @staticmethod 38 | def print_record(rows): 39 | if rows: 40 | for row in rows: 41 | for name, value in row.items(): 42 | print(f'{name}: {value}') 43 | print() 44 | else: 45 | print('no record found') 46 | 47 | def get_record(self, filters=None): 48 | if filters is None: 49 | filters = {} 50 | for name, value in filters.items(): 51 | if name in self.column_map: 52 | self.column_map[name].validate_value(value) 53 | else: 54 | raise ValueError(f"Column '{name}' not found is table") 55 | filtered_record = [] 56 | for row in self.rows: 57 | matched = True 58 | for name, value in filters.items(): 59 | if row[name] is not value: 60 | matched = False 61 | break 62 | if matched: 63 | filtered_record.append(row) 64 | return filtered_record 65 | 66 | 67 | def main(): 68 | 69 | new_columns = [ 70 | Column('id', fields.IntegerField(), [constraints.NotNullConstraint(), constraints.PrimaryKey()]), 71 | Column('name', fields.StringField(max_length=10), [constraints.NotNullConstraint()]), 72 | Column('age', fields.IntegerField(min_value=0, max_value=100), [constraints.NotNullConstraint()]) 73 | ] 74 | new_table = Table('student', columns=new_columns) 75 | new_table.add_record({'id': 1, 'name': 'shivam', 'age': 10}) 76 | new_table.add_record({'id': 2, 'name': 'shivam', 'age': 20}) 77 | new_table.add_record({'id': 3, 'name': 'jindal', 'age': 2}) 78 | new_table.print_record(new_table.get_record({'name': 'shivam', 'id': 2})) 79 | 80 | main() 81 | -------------------------------------------------------------------------------- /solutions/nokia_snake_game/main.py: -------------------------------------------------------------------------------- 1 | import random 2 | from collections import deque 3 | from enum import Enum 4 | from time import sleep 5 | 6 | class ShellType(Enum): 7 | EMPTY = 1 8 | SNAKE = 2 9 | FOOD = 3 10 | 11 | 12 | class Direction(Enum): 13 | LEFT = 1 14 | RIGHT = -1 15 | TOP = 2 16 | DOWN = -2 17 | 18 | 19 | class Shell: 20 | def __init__(self, row: int, column: int): 21 | self.row = row 22 | self.col = column 23 | self.shell_type = ShellType.EMPTY 24 | 25 | def get_row(self): 26 | return self.row 27 | 28 | def get_column(self): 29 | return self.col 30 | 31 | def set_shell_type(self, shell_type: ShellType): 32 | self.shell_type = shell_type 33 | 34 | def get_shell_tpe(self): 35 | return self.shell_type 36 | 37 | 38 | class Snake: 39 | 40 | def __init__(self, head: Shell): 41 | self.head = head 42 | self.head.set_shell_type(ShellType.SNAKE) 43 | self.snake = deque([self.head]) 44 | self.direction = Direction.RIGHT 45 | 46 | def move(self, new_shell: Shell): 47 | print(f'Moving to {new_shell.row}-{new_shell.col}') 48 | if new_shell.shell_type != ShellType.FOOD: 49 | popped_shell = self.snake.pop() 50 | popped_shell.set_shell_type(ShellType.EMPTY) 51 | self.snake.append(new_shell) 52 | new_shell.set_shell_type(ShellType.SNAKE) 53 | self.head = new_shell 54 | 55 | def get_score(self): 56 | return len(self.snake) 57 | 58 | 59 | class Board: 60 | 61 | def __init__(self, row_count, col_count): 62 | self.row_count = row_count 63 | self.col_count = col_count 64 | self.board = [[Shell(i, j) for j in range(col_count)] for i in range(row_count)] 65 | 66 | def already_have_food(self): 67 | for i in range(self.row_count): 68 | for j in range(self.col_count): 69 | if self.board[i][j].shell_type == ShellType.FOOD: 70 | return True 71 | return False 72 | 73 | def generate_food(self): 74 | while True: 75 | row = random.choice(range(self.row_count)) 76 | col = random.choice(range(self.col_count)) 77 | if self.board[row][col].shell_type == ShellType.EMPTY: 78 | self.board[row][col].set_shell_type(ShellType.FOOD) 79 | print(f'Food is at {row}-{col}') 80 | break 81 | 82 | class Game: 83 | 84 | def __init__(self, row_count, col_count): 85 | self.board = Board(row_count, col_count) 86 | self.snake = Snake(Shell(0, 0)) 87 | self.is_game_over = False 88 | 89 | def get_is_game_over(self): 90 | return self.is_game_over 91 | 92 | def get_safe_row(self, row): 93 | if row - 1 < 0: 94 | self.change_direction(Direction.DOWN) 95 | return row + 1 96 | self.change_direction(Direction.TOP) 97 | return row -1 98 | def get_safe_col(self, col): 99 | if col - 1 < 0: 100 | self.change_direction(Direction.RIGHT) 101 | return col + 1 102 | self.change_direction(Direction.LEFT) 103 | return col - 1 104 | 105 | def save_from_boundry(self, row, col): 106 | if row < 0: 107 | row += 1 108 | col = self.get_safe_col(col) 109 | elif row == self.board.row_count: 110 | row -= 1 111 | col = self.get_safe_col(col) 112 | elif col < 0: 113 | col += 1 114 | row = self.get_safe_row(row) 115 | elif col == self.board.col_count: 116 | col -= 1 117 | row = self.get_safe_row(row) 118 | return row, col 119 | 120 | def get_next_shell_cordinate(self): 121 | row = self.snake.head.row 122 | col = self.snake.head.col 123 | if self.snake.direction == Direction.RIGHT: 124 | col += 1 125 | elif self.snake.direction == Direction.LEFT: 126 | col -= 1 127 | if self.snake.direction == Direction.TOP: 128 | row -= 1 129 | elif self.snake.direction == Direction.DOWN: 130 | row += 1 131 | return self.save_from_boundry(row, col) 132 | 133 | def is_shell_safe(self, row, col): 134 | if row < 0 or row >= self.board.row_count or col < 0 or col >= self.board.col_count or self.board.board[row][col].shell_type == ShellType.SNAKE: 135 | return False 136 | return True 137 | 138 | def mark_game_over(self): 139 | self.is_game_over = True 140 | print(f'Your Score is {len(self.snake.snake)}') 141 | 142 | def play(self): 143 | time_count = 0 144 | while True: 145 | sleep(.2) 146 | time_count += 1 147 | if time_count%2 == 0: 148 | self.change_direction_random() 149 | not self.board.already_have_food() and self.board.generate_food() 150 | row, col = self.get_next_shell_cordinate() 151 | if self.is_shell_safe(row, col): 152 | self.snake.move(self.board.board[row][col]) 153 | else: 154 | self.mark_game_over() 155 | break 156 | def change_direction_random(self): 157 | if self.snake.direction == Direction.TOP: 158 | self.change_direction(random.choice([Direction.TOP, Direction.LEFT, Direction.RIGHT])) 159 | elif self.snake.direction == Direction.DOWN: 160 | self.change_direction(random.choice([Direction.DOWN, Direction.LEFT, Direction.RIGHT])) 161 | elif self.snake.direction == Direction.LEFT: 162 | self.change_direction(random.choice([Direction.TOP, Direction.LEFT, Direction.DOWN])) 163 | elif self.snake.direction == Direction.RIGHT: 164 | self.change_direction(random.choice([Direction.TOP, Direction.RIGHT, Direction.RIGHT])) 165 | 166 | def change_direction(self, direction: Direction): 167 | print(f'Changing direction to {direction}') 168 | self.snake.direction = direction 169 | 170 | 171 | game = Game(10, 10) 172 | game.play() 173 | --------------------------------------------------------------------------------