KAHIBARO
Discord Login Register

32.2. Designing the Architecture

Clarifying the Goal

In this chapter you design the architecture for your final production-ready backend project.

You are not building endpoints yet. You are deciding how the system will be structured, which parts it will have, how they talk to each other, and which responsibilities each part gets.

The focus is on:

All later chapters in the final project will plug into this architecture.

A good architecture is about clear boundaries, simple communication, and explicit responsibilities, not about using as many fancy patterns and tools as possible.

Requirements Summary

Before you design the architecture you should restate the main requirements of the final project in technical terms. The exact project idea can vary, but a realistic production backend usually needs:

From these you derive system-level requirements:

AreaRequirements example
APIJSON REST API, versioned, secure over HTTPS
DataStrong consistency, transactions, relational schema, migrations
PerformanceFast responses for common reads, pagination, caching, connection pooling
ScalabilityAbility to run multiple app instances, stateless API nodes
SecurityJWT-based auth, HTTPS, secure password storage, secure secrets
ReliabilityGraceful shutdown, health checks, background job retries, backups
OperabilityStructured logs, metrics, alerts, environment-based config

You will design an architecture that satisfies these requirements without overcomplicating it.

Choosing an Architectural Style

You have many options, but for a single backend application built by one person or a small team, you typically choose between:

For this final project, microservices are overkill. They introduce network overhead, distributed transactions, complex deployment, and a lot of operational burden.

A modular monolith is usually the best choice.

Modular Monolith Overview

A modular monolith is:

Example high-level modules:

These are logical boundaries, not separate microservices.

Use one deployable app with multiple well-defined modules. Avoid splitting into microservices unless you have strong reasons and experience to manage the added complexity.

You will also follow a layered architecture inside the monolith.

High-Level System Architecture

At the highest level, your production backend system will usually have:

  1. Clients
    • Web frontend (React / Vue / etc.), mobile apps, or external API clients.
  2. Reverse proxy / load balancer
    • Nginx or Traefik in front of the app.
    • Terminates TLS, routes requests to app containers.
  3. Application server
    • FastAPI app, run by Uvicorn + Gunicorn.
    • Stateless HTTP API.
  4. Database
    • PostgreSQL instance for relational data.
  5. Cache and message broker
    • Redis for:
      • Caching reads.
      • Storing short-lived keys (tokens, rate limits, sessions).
      • Acting as Celery broker / result backend.
  6. Background workers
    • Celery workers (or similar) running the same codebase but executing background tasks.
  7. File storage
    • Local filesystem for development.
    • Cloud object storage (S3 or compatible) for production.
  8. Observability
    • Logging system (to stdout, then collected by Docker / server).
    • Metrics endpoint for Prometheus.
    • Health check endpoints for load balancers.

You can visualize a simple request path:

Client β†’ HTTPS β†’ Nginx β†’ FastAPI (app server) β†’ PostgreSQL / Redis β†’ Response

And a background task path:

FastAPI β†’ push job to Redis (Celery) β†’ Worker consumes job β†’ DB / email / file storage

Logical Layers Inside the Application

Inside the FastAPI app you will structure code into layers. A common simple decomposition:

  1. API layer (FastAPI routes)
  2. Service / domain layer
  3. Repository / persistence layer
  4. Infrastructure / integration layer

API Layer

Responsibilities:

The API layer does not:

Example structure:

text
app/
  api/
    v1/
      users.py
      auth.py
      products.py
      orders.py

Each file defines a FastAPI router for that domain.

Service Layer

Responsibilities:

Examples of service methods:

API endpoints call service functions or classes. Services call repositories and other infrastructure.

Repository Layer

Responsibilities:

Example:

The service layer should not know about SQL, just that it can save, find, or update domain objects.

Infrastructure / Integration Layer

Responsibilities:

This layer is where you integrate third-party libraries, like:

Domain Modules and Boundaries

Within the layered structure you will have domain modules. Each module implements its own:

Example Module Breakdown

Imagine an e-commerce-like final project. A reasonable separation could be:

ModuleResponsibilities
usersUser profiles, basic data, account management
authRegistration, login, logout, tokens, password reset, email verification
productsProduct catalog, categories, pricing
ordersCarts, orders, order items, order lifecycle
paymentsPayment initiation, status tracking, webhook processing
filesFile uploads, image resizing, links, storage paths
notificationsEmail notifications, background sending, templates
adminAdmin-only operations, dashboards, data exports

Each module:

Keep data ownership within modules. For example, only the orders module should be responsible for changing an order status. Other modules should call its services instead of directly manipulating its tables.

Example Cross-Module Communication

In a modular monolith, these are function calls in the same codebase, not network requests.

Choosing the Data Flow Patterns

You will mostly use request / response for API endpoints and command-style service methods for actions.

Example flow for creating an order:

  1. Client calls POST /api/v1/orders with cart items.
  2. API layer:
    • Validates input.
    • Gets current user from auth dependency.
    • Calls OrderService.create_order(user, items).
  3. OrderService:
    • Validates products exist using ProductRepository.
    • Calculates totals, taxes, discounts.
    • Creates order and order items using OrderRepository.
    • Returns order DTO.
  4. API layer:
    • Maps order DTO to response model.
    • Returns 201 Created with order data.

For long operations, like sending emails or processing payments:

Planning for Asynchronous vs Synchronous Parts

Since you use FastAPI, you will have async capabilities. But not everything must be async.

General rules:

Structure:

Design decision:

Database and Data Access Design

At architecture level you decide:

Database Integration Pattern

You can follow a typical pattern:

Example concept:

python
def get_db_session():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Each repository method expects db: Session.

This is an example of the repository pattern, used to decouple higher layers from the ORM.

Module Ownership Over Tables

Decide table ownership:

Keep foreign keys clear and avoid circular dependencies where possible.

Caching and Redis Usage Design

You need a plan for how you will use Redis, not just "we will cache things".

Typical usages:

  1. Application caching
    • Cache common read-heavy data, such as product lists or category trees.
    • Key plan example:
      • products:list:page:{page}:{filters_hash}
      • product:{product_id}
  2. Authentication-related data
    • Optional session store for session-based auth.
    • Blacklist or "revoked" token lists if needed.
  3. Rate limiting
    • Keys like rate:user:{user_id}:{window_start}.
  4. Background processing
    • Redis as Celery broker and result backend.

General caching rules:

Design cache keys with:

  • A clear prefix for the feature (for example products:, users:).
  • No sensitive data in the key or value.
  • Expiration (TTL) to prevent stale data from living forever.

You will not implement all caching logic here, but your architecture should consider:

Background Processing Design

You must plan which tasks will be executed in the background and how they are connected.

Typical background jobs:

Architecture:

Communication:

python
  send_welcome_email.delay(user_id)

You also plan:

File Storage Architecture

You must decide how to handle files such as images or documents.

Design decisions:

By abstracting file storage behind a service, you can:

Configuration and Environment Separation

Your architecture must support different environments:

Configuration design:

Configuration is injected, not hard-coded, so you can:

Cross-Cutting Concerns

Some parts of your architecture cross module boundaries. You must plan how to handle them centrally.

Logging

Design:

Metrics and Health Checks

Design:

Security

Many security features are part of other chapters but architecturally you must:

Define clear boundaries:

Error Handling and Validation

You need a consistent plan for how errors flow through the system.

Examples:

By centralizing exception handling in a dedicated module (for example app.api.errors), your endpoints can remain clean and your architecture predictable.

Example Directory Structure

A concrete example for your final project:

text
app/
  core/
    config.py          # Settings classes
    security.py        # JWT, password hashing
    logging.py         # Logging setup
    exceptions.py      # Base exceptions
  db/
    base.py            # Base models, metadata
    session.py         # SessionLocal, engine
    migrations/        # Alembic migrations
  api/
    deps.py            # Common dependencies
    errors.py          # Exception handlers
    v1/
      users.py
      auth.py
      products.py
      orders.py
      payments.py
      files.py
      admin.py
  users/
    models.py
    schemas.py
    repository.py
    service.py
  auth/
    models.py
    schemas.py
    repository.py
    service.py
    tokens.py
  products/
    models.py
    schemas.py
    repository.py
    service.py
  orders/
    models.py
    schemas.py
    repository.py
    service.py
    tasks.py
  payments/
    models.py
    schemas.py
    repository.py
    service.py
    gateway.py
  files/
    models.py
    schemas.py
    storage.py
    service.py
    tasks.py
  notifications/
    email_service.py
    tasks.py
  tasks/
    celery_app.py      # Celery configuration, app instance
  main.py              # FastAPI app factory

This structure shows:

Architectural Trade-Offs and Constraints

Every architecture is a compromise. You should explicitly accept some trade-offs:

Make sure your design supports:

Summary and Next Steps

You now have:

In the following chapters you will:

Keep this architecture as your reference while implementing features. If you feel tempted to add a shortcut that breaks boundaries, consider whether it is worth the future complexity or if you can extend the architecture cleanly instead.

Views: 4

Comments

Please login to add a comment.

Don't have an account? Register now!