KAHIBARO
Discord Login Register

31.1. Architecture

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:

  1. Clients
    • Web frontend (React, Vue, plain HTML, etc.)
    • Mobile app
    • Admin dashboard
    • Third‑party integrations (for example payment provider webhooks)
  2. API Backend
    • HTTP REST API
    • Authentication and authorization
    • Validation and business logic
    • Background job dispatch (send email, update inventory, etc.)
  3. 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:

  1. Client sends HTTP request to API Gateway / Reverse Proxy (for example Nginx).
  2. Request is passed to Application Server (for example FastAPI + Uvicorn).
  3. Request enters API layer (routing, authentication, validation).
  4. API layer calls Service layer (business logic).
  5. Service layer uses Repository / ORM layer to talk to PostgreSQL and Redis.
  6. Service layer may enqueue background tasks (for example send email).
  7. 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:

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:

ModuleResponsibility
usersRegistration, login, profiles, addresses, roles (customer vs admin)
productsProducts, SKUs, attributes, pricing, stock information
categoriesOrganizing products into categories and subcategories
cartShopping carts per user or per session, line items, discounts at cart level
ordersConverting carts to orders, order statuses, order history
paymentsPayment intents, payment provider integration, capturing and refunds
inventoryStock reservations, stock changes after orders, backorders
backgroundEmail notifications, async tasks, cleanup jobs
adminAdmin‑only operations (manage products, orders, discounts)
commonShared 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:

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:

  1. API / Presentation Layer
    • Routers / controllers
    • Request and response models (DTOs)
    • HTTP concerns (status codes, headers, query params)
  2. Service / Domain Layer
    • Business rules
    • Use cases such as "add item to cart", "place order", "refund payment"
    • Transactions and consistency across operations
  3. Persistence Layer (Repositories / ORM)
    • Data access logic
    • Database models
    • Queries, inserts, updates
  4. 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:

  1. POST /orders (API layer) receives the request.
  2. API layer calls OrderService.place_order(user_id, request_data).
  3. OrderService:
    • Reads the current cart through CartRepository.
    • Validates stock through InventoryService.
    • Creates an Order through OrderRepository.
    • Initiates payment through PaymentService.
  4. OrderService returns 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:

text
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.py

This is just an example, not a strict template. The key idea is that each feature has:

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:

  1. GET /products?category_id=10&page=2 hits the API layer.
  2. API layer:
    • Validates query parameters.
    • Converts them to domain types, for example CategoryId, Page.
    • Calls ProductService.list_products(category_id, pagination).
  3. ProductService:
    • Uses ProductRepository to load products from the database.
    • Applies business rules such as excluding hidden or deleted products.
    • Possibly uses cache through CacheService.
  4. Data is mapped to response schema and returned to the client.

Responsibility separation:

Example 2: Adding to Cart

Use case: User adds a product to their cart.

Flow:

  1. POST /cart/items with {"product_id": 123, "quantity": 2}.
  2. API layer:
    • Authenticates user.
    • Validates request body with a request schema.
    • Calls CartService.add_item(user_id, product_id, quantity).
  3. CartService:
    • Retrieves or creates current cart with CartRepository.
    • Checks product existence and price through ProductRepository or a ProductService.
    • Optionally checks inventory with InventoryService.
    • Updates cart items, recalculates cart total.
    • Persists changes with CartRepository.
  4. 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:

  1. POST /orders/checkout with address, delivery method, and payment method.
  2. API layer:
    • Authenticates user.
    • Validates request data.
    • Calls OrderService.checkout(user_id, checkout_data).
  3. OrderService.checkout:
    • Reads cart via CartRepository.
    • Validates cart (not empty, prices still valid, etc.).
    • Starts a database transaction.
    • Interacts with InventoryService to reserve stock.
    • Creates an Order and OrderItem records via OrderRepository.
    • Calls PaymentService to create a payment intent with external provider.
    • Commits the transaction.
    • Schedules background tasks such as "send order confirmation email".
  4. 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:

ConcernWhere to put itUsed by modules
Authenticationcore.security, API dependenciesusers, orders, cart, admin
AuthorizationAPI dependencies / decoratorsadmin, orders, products
Loggingcore.logging, middlewareAll
Cachinginfrastructure.cacheproducts, categories, orders
Paginationcore.pagination utilitiesproducts, orders, admin listings
Error handlingexceptions + global handlersAll
Validation rulesdomain services and schemasAll

For example, you might have a core.exceptions file with custom exceptions:

python
class DomainError(Exception):
    pass
class OutOfStockError(DomainError):
    pass
class PaymentFailedError(DomainError):
    pass

Then 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:

Typical approach in a monolith:

Example pattern (pseudocode):

python
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 order

Here you:

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:

You should not call these external APIs directly from your service or API layer. Instead, you build adapters in the infrastructure layer.

Example idea:

python
# 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:

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:

Where to place caching:

Example idea:

python
# 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 product

The 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:

Architecturally:

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:

  1. OrderService.checkout completes and commits transaction.
  2. OrderService calls BackgroundTasksService.enqueue("send_order_email", order_id).
  3. 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?

If you follow the patterns in this chapter:

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

Comments

Please login to add a comment.

Don't have an account? Register now!