AlgoDaily

Backend Architecture Patterns

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Architecture After Design

From system ideas to code shape

In Introduction to Databases, you built the mental model for persistence: tables, documents, transactions, indexes, and queries.

In System Design Principles, you practiced thinking like a senior engineer: estimate first, find bottlenecks, design for failure, and treat consistency as a product decision.

Now we move one layer closer to implementation: backend architecture patterns.

Architecture patterns answer questions like:

  • Where should business rules live?
  • How should APIs talk to databases?
  • When should services call each other directly vs publish events?
  • How do we keep a backend understandable as it grows?

A senior backend engineer does not pick patterns because they sound impressive. They pick patterns to make change safer, ownership clearer, and failure easier to reason about.

Architecture After Design

Pattern Mental Model

Architecture is dependency design

Most backend architecture patterns are really about controlling dependencies.

If every part of your code can call every other part, your system becomes hard to change:

Controller -> Database
Scheduler  -> Controller
Service    -> External API
Database   -> Business Logic? 😬

Patterns create intentional boundaries:

External world
     ↓
Interface / API layer
     ↓
Application / use-case layer
     ↓
Domain rules
     ↓
Infrastructure adapters

The goal is not ceremony. The goal is to make this obvious:

  • What is core business behavior?
  • What is delivery mechanism, like HTTP or CLI?
  • What is infrastructure, like PostgreSQL, Redis, queues, or third-party APIs?
  • What can change without rewriting everything else?

A useful senior-level question is: If this dependency changes, how much code must move?

Pattern Mental Model

Layered Architecture

The default backend pattern

Layered architecture is often the first clean structure for a backend:

HTTP/API layer
   ↓
Service layer
   ↓
Repository/data access layer
   ↓
Database

Each layer has a job:

  • Controller/API layer: parse requests, return responses
  • Service layer: coordinate business actions
  • Repository layer: hide storage details
  • Database: persist state

This pattern works well when your app is mostly request/response CRUD plus moderate business logic.

The senior move is not merely creating folders named controllers, services, and repositories. The senior move is enforcing the rule: business decisions should not leak into transport or storage code.

```python
class UserRepository:
    def __init__(self):
        self.users = {}

    def save(self, user_id, user):
        self.users[user_id] = user

    def find(self, user_id):
        return self.users.get(user_id)


class UserService:
    def __init__(self, repository):
        self.repository = repository

    def register_user(self, user_id, email):
        if "@" not in email:
            return {"ok": False, "error": "Invalid email"}

        user = {"id": user_id, "email": email, "status": "active"}
        self.repository.save(user_id, user)
        return {"ok": True, "user": user}


class UserController:
    def __init__(self, service):
        self.service = service

    def post_users(self, request_body):
        result = self.service.register_user(
            request_body["id"],
            request_body["email"]
        )
        return {"status_code": 201 if result["ok"] else 400, "body": result}


repo = UserRepository()
service = UserService(repo)
controller = UserController(service)

print(controller.post_users({"id": 1, "email": "ada@example.com"}))
print(controller.post_users({"id": 2, "email": "not-an-email"}))
print("Stored user:", repo.find(1))
```

Layer Responsibility Check

In a layered backend, the controller/API layer should contain most business rules because it is closest to the user request.

Hexagonal Architecture

Put the business core in the center

Hexagonal architecture is also called ports and adapters.

Its core idea:

Your application should not depend directly on databases, web frameworks, message queues, or payment providers.

Instead, the business core defines ports: interfaces describing what it needs. Infrastructure provides adapters: concrete implementations.

          HTTP Adapter
              ↓
Payment Adapter -> Application Core <- Database Adapter
              ↑
          Queue Adapter

This helps when external systems change. For example, switching from one payment provider to another should not rewrite checkout rules.

Use this pattern when business logic is valuable, long-lived, and likely to outlast specific infrastructure choices.

```python
from typing import Protocol


class PaymentPort(Protocol):
    def charge(self, user_id: int, cents: int) -> str:
        ...


class FakeStripeAdapter:
    def charge(self, user_id: int, cents: int) -> str:
        return f"stripe_charge_id=user-{user_id}-amount-{cents}"


class CheckoutService:
    def __init__(self, payment_port: PaymentPort):
        self.payment_port = payment_port

    def checkout(self, user_id: int, cart_total_cents: int):
        if cart_total_cents <= 0:
            return {"ok": False, "error": "Cart total must be positive"}

        charge_id = self.payment_port.charge(user_id, cart_total_cents)
        return {"ok": True, "charge_id": charge_id}


payments = FakeStripeAdapter()
checkout = CheckoutService(payments)

print(checkout.checkout(user_id=42, cart_total_cents=2599))
print(checkout.checkout(user_id=42, cart_total_cents=0))
```

Ports And Adapters

In hexagonal architecture, infrastructure code that translates an external system into something the application core can use is called an _____.

Modular Monolith First

Monolith does not mean messy

A modular monolith is a single deployable backend with strong internal module boundaries.

Example modules:

billing/
orders/
inventory/
notifications/
users/

Each module owns its concepts and exposes intentional entry points. Other modules should not casually reach into its tables, internal classes, or private workflows.

This is often a better senior choice than jumping straight to microservices because it keeps operational complexity low while still preparing the codebase for growth.

Good modular monolith signs:

  • Clear module ownership
  • Few cross-module imports
  • Shared code is boring and stable
  • Data ownership is explicit
  • Boundaries match business capabilities

Microservices can be powerful, but splitting too early often converts simple code problems into distributed systems problems.

Modular Monolith First

Microservice Boundary

A team wants to split orders and billing into separate microservices. Which reason is the strongest architectural justification?

Event Driven Flow

Decouple with events

In request/response architecture, one component directly asks another to do something:

Order Service -> Email Service

In event-driven architecture, a component announces that something happened:

Order Service -> publishes OrderPlaced
Inventory, Email, Analytics -> react independently

This is useful when multiple downstream behaviors should happen after a fact, but the original action should not know every consumer.

Events are especially helpful for:

  • Notifications
  • Auditing
  • Analytics pipelines
  • Cache updates
  • Cross-module reactions

But events also introduce tradeoffs from System Design Principles: retries, ordering, duplicate messages, delayed consistency, and harder debugging.

A senior backend engineer treats events as a contract, not just a convenient callback.

```python
class EventBus:
    def __init__(self):
        self.handlers = {}

    def subscribe(self, event_name, handler):
        self.handlers.setdefault(event_name, []).append(handler)

    def publish(self, event_name, payload):
        print(f"Publishing {event_name}: {payload}")
        for handler in self.handlers.get(event_name, []):
            handler(payload)


def reserve_inventory(event):
    print(f"Inventory reserved for order {event['order_id']}")


def send_confirmation_email(event):
    print(f"Email sent to user {event['user_id']} for order {event['order_id']}")


bus = EventBus()
bus.subscribe("OrderPlaced", reserve_inventory)
bus.subscribe("OrderPlaced", send_confirmation_email)

order = {"order_id": "ord_1001", "user_id": 7, "total_cents": 4999}
bus.publish("OrderPlaced", order)
```

Choosing The Pattern

Pattern selection checklist

Architecture patterns are tools, not badges. Choose based on the pressure your backend is under.

Use layered architecture when
  • The app is request/response heavy
  • Business logic is moderate
  • A simple team structure needs clarity
  • You want a clean baseline quickly
Use hexagonal architecture when
  • Business rules are central and long-lived
  • Infrastructure may change
  • You need strong isolation from frameworks and vendors
  • Use cases should be callable from HTTP, jobs, CLI, or messages
Use a modular monolith when
  • The product is growing but does not need distributed deployment yet
  • You want service-like boundaries without service-like operations
  • Teams can own modules inside one deployable
Use microservices when
  • Independent deployment is truly valuable
  • Different parts scale differently
  • Failures need isolation
  • Teams can handle distributed operations
Use event-driven architecture when
  • Many consumers react to the same fact
  • Producers should not know all downstream workflows
  • Delayed consistency is acceptable

Next, Advanced Languages and Frameworks will connect these architecture ideas to real implementation tools without letting the tools drive the design.

Choosing The Pattern

Senior Architecture Review

You are reviewing a backend design. The API handlers validate HTTP input, calculate discounts, directly write SQL, call a payment provider, and send emails. What is the best senior-level critique?

Open interactive view →