31.1. Architecture
Table of Contents
Big Picture of the E‑Commerce Backend
Before writing any code, you need a clear mental picture of how the e‑commerce backend fits together. Architecture is that picture. In this chapter you will design the structure of the project at a high level so that later chapters (users, products, cart, orders, payments, caching, background jobs) can plug into it in a clean way.
We will not go deep into syntax or specific frameworks here. Instead, you will learn how to split the e‑commerce domain into modules, layers, and services that are easy to understand, test, and extend.
Goal: Design a clear, modular, layered architecture for an e‑commerce backend so that every feature (user, product, cart, orders, payments) has an obvious place to live.
High‑Level System Overview
At a high level, your e‑commerce backend will look like this:
- Clients
- Web frontend (React, Vue, plain HTML, etc.)
- Mobile app
- Admin dashboard
- Third‑party integrations (for example payment provider webhooks)
- API Backend
- HTTP REST API
- Authentication and authorization
- Validation and business logic
- Background job dispatch (send email, update inventory, etc.)
- Datastores and Infrastructure
- PostgreSQL (main database)
- Redis (caching, sessions, rate limiting, background queues)
- Object storage (for example S3) for product images and invoices
- Message broker / queue (often Redis) for background jobs
- External services (payment provider, email provider)
You can imagine the data and control flow in a simple sequence:
- Client sends HTTP request to API Gateway / Reverse Proxy (for example Nginx).
- Request is passed to Application Server (for example FastAPI + Uvicorn).
- Request enters API layer (routing, authentication, validation).
- API layer calls Service layer (business logic).
- Service layer uses Repository / ORM layer to talk to PostgreSQL and Redis.
- Service layer may enqueue background tasks (for example send email).
- Response bubbles back through API layer to the client.
The details of web servers, Docker, etc. are covered in other chapters, so here you only need to know how these pieces conceptually talk to each other.
Choosing an Architectural Style for This Project
There are many possible architectures. For this project, we will keep it simple and realistic:
- Monolithic backend
One deployable backend application that contains all features (users, products, orders, payments, etc.). - Layered architecture with modular structure
You will organize code into: - Presentation layer (HTTP / API)
- Service / domain layer (business logic)
- Persistence layer (repositories / ORM)
- Infrastructure (email, payments, background jobs, storage)
- Domain‑oriented modules
Even in a monolith, you can group code by features such as: usersproductscategoriescartorderspaymentsinventoryadmincommonorcoreutilities
This gives you a modular monolith. Everything runs in one process and one database, but the code is organized as if they were separate services. This makes future refactoring easier.
Core Domain Modules for an E‑Commerce System
You will use later chapters (User Management, Product Management, etc.) to build each feature. Here you decide what your main domain modules are and how they interact.
A reasonable module breakdown:
| Module | Responsibility |
|---|---|
users | Registration, login, profiles, addresses, roles (customer vs admin) |
products | Products, SKUs, attributes, pricing, stock information |
categories | Organizing products into categories and subcategories |
cart | Shopping carts per user or per session, line items, discounts at cart level |
orders | Converting carts to orders, order statuses, order history |
payments | Payment intents, payment provider integration, capturing and refunds |
inventory | Stock reservations, stock changes after orders, backorders |
background | Email notifications, async tasks, cleanup jobs |
admin | Admin‑only operations (manage products, orders, discounts) |
common | Shared utilities, custom exceptions, base models |
You will not fully design each module here, but you must understand the dependencies between them.
A simplified dependency view:
ordersdepends on:cart(to create an order from a cart)users(customer)productsandinventorypaymentscartdepends on:productsusers(if authenticated carts)- possibly
discountsif you introduce them paymentsdepends on:orders- external payment provider
inventorydepends on:productsordersandcart(for stock reservations and deductions)
Rule: Domain modules should depend on each other in a clear, one‑directional way. Avoid circular dependencies like orders importing payments and payments importing orders in the same layer.
Layered Architecture Inside the Monolith
To keep your codebase maintainable, you will use a layered architecture. The specific names may vary, but at minimum you have:
- API / Presentation Layer
- Routers / controllers
- Request and response models (DTOs)
- HTTP concerns (status codes, headers, query params)
- Service / Domain Layer
- Business rules
- Use cases such as "add item to cart", "place order", "refund payment"
- Transactions and consistency across operations
- Persistence Layer (Repositories / ORM)
- Data access logic
- Database models
- Queries, inserts, updates
- Infrastructure Layer
- Email client
- Payment provider client
- Cache (Redis) adapter
- Background job dispatcher
- File storage client
A simple illustration for the "place order" use case:
POST /orders(API layer) receives the request.- API layer calls
OrderService.place_order(user_id, request_data). OrderService:- Reads the current cart through
CartRepository. - Validates stock through
InventoryService. - Creates an
OrderthroughOrderRepository. - Initiates payment through
PaymentService. OrderServicereturns a result that the API layer converts to a HTTP response.
The API layer does not talk directly to the database or the payment provider. It only talks to services. The services decide which repositories and infrastructure components to use.
Rule: API routes should be very thin. All real business logic goes into service classes or functions.
Example Project Structure
Exact naming will depend on your framework, but you can use a structure similar to this:
app/
main.py # App entrypoint
config.py # Settings and configuration
db/
base.py # DB session / engine setup
models/ # SQLAlchemy models
user.py
product.py
category.py
cart.py
order.py
payment.py
inventory.py
api/ # Presentation layer
deps.py # Common dependencies (auth, DB session, etc.)
v1/
users.py
products.py
categories.py
cart.py
orders.py
payments.py
admin.py
services/ # Business logic / use cases
users.py
products.py
categories.py
cart.py
orders.py
payments.py
inventory.py
repositories/ # Database access layer
users.py
products.py
categories.py
cart.py
orders.py
payments.py
inventory.py
infrastructure/ # External services and technical concerns
email.py
payments.py
storage.py
cache.py
background_tasks.py
schemas/ # Pydantic models (request / response DTOs)
users.py
products.py
categories.py
cart.py
orders.py
payments.py
core/ # Shared utilities and cross‑cutting concerns
security.py
exceptions.py
logging.py
pagination.pyThis is just an example, not a strict template. The key idea is that each feature has:
- Database models
- Repositories
- Services
- API routes
- Schemas
and that these are grouped clearly by feature.
Typical Request Flows in E‑Commerce
To design architecture, it helps to walk through common user actions and ask: Where does each step belong?
Example 1: Browsing Products
Use case: A user wants to see a list of products in a category.
Flow:
GET /products?category_id=10&page=2hits the API layer.- API layer:
- Validates query parameters.
- Converts them to domain types, for example
CategoryId,Page. - Calls
ProductService.list_products(category_id, pagination). ProductService:- Uses
ProductRepositoryto load products from the database. - Applies business rules such as excluding hidden or deleted products.
- Possibly uses cache through
CacheService. - Data is mapped to response schema and returned to the client.
Responsibility separation:
- No SQL in
products.pyrouter. - No HTTP logic (status codes, headers) in
ProductService. - No product filtering rules inside database setup files.
Example 2: Adding to Cart
Use case: User adds a product to their cart.
Flow:
POST /cart/itemswith{"product_id": 123, "quantity": 2}.- API layer:
- Authenticates user.
- Validates request body with a request schema.
- Calls
CartService.add_item(user_id, product_id, quantity). CartService:- Retrieves or creates current cart with
CartRepository. - Checks product existence and price through
ProductRepositoryor aProductService. - Optionally checks inventory with
InventoryService. - Updates cart items, recalculates cart total.
- Persists changes with
CartRepository. - Returns updated cart or success information.
Note that CartService is the main actor here. If you ever move to microservices, this service logic can move to a dedicated cart service, but the API route remains thin.
Example 3: Checkout and Order Creation
Use case: User checks out and creates an order.
Flow:
POST /orders/checkoutwith address, delivery method, and payment method.- API layer:
- Authenticates user.
- Validates request data.
- Calls
OrderService.checkout(user_id, checkout_data). OrderService.checkout:- Reads cart via
CartRepository. - Validates cart (not empty, prices still valid, etc.).
- Starts a database transaction.
- Interacts with
InventoryServiceto reserve stock. - Creates an
OrderandOrderItemrecords viaOrderRepository. - Calls
PaymentServiceto create a payment intent with external provider. - Commits the transaction.
- Schedules background tasks such as "send order confirmation email".
- Returns order summary and payment information.
Here the OrderService coordinates multiple modules: cart, inventory, payments, and background jobs. That is exactly what the service / domain layer is for.
Handling Cross‑Cutting Concerns
Some features do not belong to a single module. They should be designed as cross‑cutting concerns and encapsulated in reusable components.
Examples:
| Concern | Where to put it | Used by modules |
|---|---|---|
| Authentication | core.security, API dependencies | users, orders, cart, admin |
| Authorization | API dependencies / decorators | admin, orders, products |
| Logging | core.logging, middleware | All |
| Caching | infrastructure.cache | products, categories, orders |
| Pagination | core.pagination utilities | products, orders, admin listings |
| Error handling | exceptions + global handlers | All |
| Validation rules | domain services and schemas | All |
For example, you might have a core.exceptions file with custom exceptions:
class DomainError(Exception):
pass
class OutOfStockError(DomainError):
pass
class PaymentFailedError(DomainError):
passThen global exception handlers in your API layer transform these into consistent HTTP error responses.
Rule: Cross‑cutting concerns should live in shared modules, not be re‑implemented in each endpoint or service.
Transactions and Data Consistency
E‑commerce needs strong consistency around orders, payments, and inventory. Your architecture needs a clear policy:
- When do you start a database transaction?
- Which operations must happen atomically?
Typical approach in a monolith:
- Each service method that performs a critical business operation is responsible for the transaction.
Example pattern (pseudocode):
def place_order(user_id: int) -> Order:
with db_session() as session:
cart = cart_repo.get_active_cart_for_user(session, user_id)
inventory_service.reserve_stock(session, cart.items)
order = order_repo.create_from_cart(session, cart)
payment = payment_service.create_payment_intent(session, order)
cart_repo.mark_as_converted(session, cart)
session.commit()
return orderHere you:
- Use a single session / transaction for:
- Reserving stock
- Creating order
- Recording payment intent
- Updating cart
- Commit once at the end.
Background tasks like sending an email are triggered after the transaction commits, or are designed to be idempotent.
Working with External Services
Your e‑commerce backend will talk to services like:
- Payment gateways (Stripe, PayPal, etc.)
- Email providers (SendGrid, SES)
- Storage (S3)
You should not call these external APIs directly from your service or API layer. Instead, you build adapters in the infrastructure layer.
Example idea:
# infrastructure/payments.py
class PaymentGatewayClient:
def create_payment_intent(self, amount_cents: int, currency: str, metadata: dict) -> PaymentIntent:
# Call external API, handle errors, map response
...
# services/payments.py
class PaymentService:
def __init__(self, payment_client: PaymentGatewayClient, payment_repo: PaymentRepository):
self.payment_client = payment_client
self.payment_repo = payment_repo
def create_payment_for_order(self, order: Order) -> Payment:
intent = self.payment_client.create_payment_intent(
amount_cents=order.total_cents,
currency=order.currency,
metadata={"order_id": order.id},
)
return self.payment_repo.save_intent(order, intent)This separation lets you:
- Swap payment providers with minimal changes.
- Mock the
PaymentGatewayClientin tests.
The same pattern works for email and storage clients.
Caching Strategy at the Architectural Level
You will see caching in detail in later chapters, but here you define where caching lives.
Typical caching targets in e‑commerce:
- Product lists and details
- Category trees
- Frequently accessed order summaries (for example on account dashboard)
- Rate limiting and sessions
Where to place caching:
- Implement technical Redis access in
infrastructure.cache. - Implement high‑level caching logic in services, for example
ProductServicedecides when to cache products.
Example idea:
# services/products.py
class ProductService:
def __init__(self, repo: ProductRepository, cache: CacheService):
self.repo = repo
self.cache = cache
def get_product(self, product_id: int) -> Product:
cache_key = f"product:{product_id}"
cached = self.cache.get(cache_key)
if cached is not None:
return cached
product = self.repo.get_by_id(product_id)
self.cache.set(cache_key, product, ttl_seconds=300)
return productThe API layer does not know if the product came from cache or the database.
Synchronous vs Background Work
Not all work should happen in the request‑response cycle. Some tasks are better handled as background jobs:
- Sending order confirmation emails
- Generating invoices
- Updating search indexes
- Cleaning up abandoned carts
Architecturally:
- Services create commands or messages that describe jobs.
- A background worker process subscribes to a queue and performs these jobs.
For this project you can use Redis as a simple queue and something like Celery or a custom worker loop.
Example flow when an order is placed:
OrderService.checkoutcompletes and commits transaction.OrderServicecallsBackgroundTasksService.enqueue("send_order_email", order_id).- Worker receives the job later and calls
EmailService.send_order_confirmation(order_id).
This keeps the checkout response fast and isolates failures in email sending from the main business transaction.
Handling Growth and Future Microservices
You are building a monolith, but you should design with future growth in mind.
Which modules could become microservices one day?
productsandcategoriesas a catalog servicecartandordersas an order servicepaymentsas a payment serviceinventoryas an inventory service
If you follow the patterns in this chapter:
- Clear service boundaries
- Thin controllers
- Separated infrastructure
then you can later move, for example, the order and payment logic to a separate service and expose it via REST or messaging with limited code changes.
You do not need to implement microservices now. You only need to avoid tight coupling that would make that move impossible.
Summary of Architectural Guidelines
To close this chapter, here are the key rules you will follow while building the e‑commerce backend:
- Use a modular monolith with domain‑oriented modules (users, products, cart, orders, payments, inventory, admin).
- Organize code in layers: API, services, repositories, infrastructure.
- Keep API routes thin and put all real business rules into services.
- Implement repositories for database access and avoid SQL in services and routes.
- Put cross‑cutting concerns like auth, logging, pagination, and error handling into shared modules.
- Use transactions around critical operations like checkout and payment.
- Access external systems (payments, email, storage, cache) through infrastructure adapters, not directly.
- Use background jobs for slow or non‑critical tasks, such as emails and cleanups.
With this architecture in mind, you are ready to design and implement the individual parts of your e‑commerce backend in the following chapters.
Views: 7
KAHIBARO