A module is a self-contained package of related functionality in our backend codebase. It encapsulates one domain concept (e.g., accounts, orders, payments) and exposes a clear interface for other parts of the system.
This document covers:
- Why we structure code into modules and the benefits of our layout.
- What each folder and file in a module does.
- A diagram that shows how the layers interact at runtime.
Video explanation: Backend Architecture Overview
- Separation of Concerns: Clearly divides HTTP routing, business logic, data access, and utilities into distinct layers.
- Reusability: Public-facing APIs (
account_service.py,types.py) allow other modules to integrate without knowing internal details. - Testability: Small, focused components (reader, writer, util) can be unit‑tested in isolation.
- Consistency: Applying the same pattern across modules speeds up onboarding and reduces cognitive load.
- Scalability: New features or entirely new domains can be added by copying the template and filling in domain specifics.
flowchart LR
HTTP[HTTP Client / Flask Request]
API[rest_api → AccountView]
Service[account_service.py]
Reader[internal/account_reader.py] & Writer[internal/account_writer.py]
Repo[internal/store/account_repository.py]
MongoDB[(MongoDB)]
HTTP --> API --> Service --> Reader --> Repo --> MongoDB
Service --> Writer --> Repo --> MongoDB
Service --> AuthenticationService
Service --> NotificationService
- HTTP Layer (
rest_api/): Routing and request/response handling. - Service Layer: Business logic, orchestration, calls to external services.
- Persistence Layer: Reader/Writer + Repository + Model (MongoDB).
- Utilities & Types: Shared helpers (utils) and data models (DTOs).
<module_name>/
├── <module_name>_service.py # Public API for other modules
├── internal/ # Implementation details (not imported externally)
│ ├── store/ # DB model & repository
│ │ ├── *_model.py
│ │ └── *_repository.py
│ ├── *_reader.py # Read operations
│ ├── *_writer.py # Write operations
│ └── *_util.py # Conversion, validation, common helpers
├── rest_api/ # HTTP routes & handlers
│ ├── *_rest_api_server.py
│ ├── *_router.py
│ └── *_view.py
├── types.py # Data Transfer Objects (DTOs)
└── errors.py # Module-specific exception classes
Note: Replace
<module_name>and*with your module’s actual name.
We will refer to the account module throughout this document to demonstrate each concept.
account/
├── account_service.py
├── internal/
│ ├── store/
│ │ ├── account_model.py
│ │ └── account_repository.py
│ ├── account_reader.py
│ ├── account_writer.py
│ └── account_util.py
├── rest_api/
│ ├── account_rest_api_server.py
│ ├── account_router.py
│ └── account_view.py
├── types.py
└── errors.py
- Role
- Exposes module‐wide operations as static methods, e.g.
create_account_by_username_and_password,reset_account_password,get_account_by_id,update_account_profile, plus wiring into AuthenticationService (for OTP/password) and NotificationService (for preferences).
- Exposes module‐wide operations as static methods, e.g.
- Imports
from modules.account.internal.account_reader import AccountReader from modules.account.internal.account_writer import AccountWriter from modules.account.types import ( Account, CreateAccountByUsernameAndPasswordParams, ResetPasswordParams, UpdateAccountProfileParams, ... ) from modules.authentication.authentication_service import AuthenticationService from modules.notification.notification_service import NotificationService
- Example call
AccountService.create_account_by_username_and_password( params=CreateAccountByUsernameAndPasswordParams(username="alice", password="secret") )
- A
@dataclassextendingBaseModel - Defines all Mongo fields (e.g.
first_name,hashed_password,phone_number,username,active,created_at,updated_at) @staticmethod from_bson()to validate & hydrate a model from raw BSON@staticmethod get_collection_name()returns"accounts"
class AccountRepository(ApplicationRepository)- Provides:
collection()— the MongoCollectionobjecton_init_collection()— sets up JSON-Schema validation (viacreate_collection) and any indexes
- Central place for low-level DB concerns
class AccountReader:- High-level read methods, e.g.
get_account_by_id(params: AccountSearchByIdParams) -> Accountget_account_by_phone_number(phone_number: PhoneNumber) -> Accountget_account_by_username_and_password(params: AccountSearchParams) -> Account
- Uses
AccountRepository.collection().find_one(...) - Converts raw BSON → domain via
AccountUtil.convert_account_bson_to_account() - Raises module-specific exceptions if not found or duplicates
- High-level read methods, e.g.
class AccountWriter:- High-level write methods, e.g.
create_account_by_username_and_password(params: CreateAccountByUsernameAndPasswordParams) -> Accountcreate_or_update_account_notification_preferences(...) -> AccountNotificationPreferencesupdate_account_profile(account_id: str, params: UpdateAccountProfileParams) -> Accountreset_account_password(params: ResetPasswordParams) -> Account
- Handles:
- Phone-number validation via
phonenumbers.parse&is_valid_number - Password hashing via
AccountUtil.hash_password() - Mongo
insert_one/find_one_and_update - Not-found errors (
AccountWithIdNotFoundError)
- Phone-number validation via
- High-level write methods, e.g.
class AccountUtil:hash_password(password: str) -> strcompare_password(password: str, hashed_password: str) -> boolconvert_account_bson_to_account(bson: dict) -> Account(usesAccountModel.from_bson)
All of the data transfer objects (DTOs) are @dataclasses, for instance:
@dataclass(frozen=True)
class CreateAccountByUsernameAndPasswordParams:
username: str
password: str
@dataclass(frozen=True)
class Account:
id: str
first_name: str
last_name: str
username: str
phone_number: PhoneNumber
hashed_password: strClients import these for type safety.
Custom AppError subclasses, e.g.:
class AccountWithUserNameExistsError(AppError): ...
class AccountWithPhoneNumberNotFoundError(AppError): ...
class AccountNotFoundError(AppError): ...Each carries its own HTTP status code and error code from AccountErrorCode in types.py.
Bootstraps a Flask Blueprint:
def create() -> Blueprint:
bp = Blueprint("account", __name__)
return AccountRouter.create_route(blueprint=bp)Registers URL rules on the Blueprint:
blueprint.add_url_rule("/accounts", view_func=AccountView.as_view("accounts"))
blueprint.add_url_rule("/accounts/<id>", view_func=AccountView.as_view("accounts_by_id"), methods=["GET", "PATCH"])
blueprint.add_url_rule(
"/accounts/<account_id>/notification-preferences",
view_func=AccountView.update_account_notification_preferences,
methods=["PATCH"],
)class AccountView(MethodView):
- Uses
flask.requestto parse JSON - Marshals params into dataclasses (e.g.
CreateAccountByPhoneNumberParams(**request.json)) - Calls
AccountService.* - Returns
jsonify(asdict(result)), <status_code> - Raises
AccountBadRequestErrorfor missing/invalid inputs