Filter concepts by levelShowing all levels.

Python · Architecture and Design Patterns

Common architecture styles

Concepts
3

Four ways to draw the boundary between business logic and infrastructure (layered, modular monolith, clean, hexagonal), domain-driven design's vocabulary for modeling the business itself, and the two ways to split a system beyond one process (microservices, event-driven).

This section

Structuring an application

Where the boundary between business rules and infrastructure goes, how the business's own vocabulary gets modeled, and how a system splits beyond one process.

Layered, modular monolith, clean, and hexagonal architecture

standardintermediate

All four are ways to stop business logic from depending directly on a database or a web framework. Layered stacks responsibilities top to bottom; modular monolith groups by feature instead of layer; clean and hexagonal both point every dependency inward, toward the business rules.

Think of it as

Layered architecture is a one-way street: presentation calls service, service calls repository, repository calls the database — each layer only knows the one below it. Clean and hexagonal architecture redraw that street as a circle: business rules sit in the center depending on nothing, and the database, the web framework, and the CLI are all replaceable plugins on the outside that depend inward on the business rules, never the reverse. A modular monolith keeps everything in one deployable process but slices it by feature (orders, billing, shipping) instead of by layer, so each slice can still be layered internally.

python
# The dependency always points one way: presentation -> service -> repository
class OrderRepository:
    def get(self, order_id): ...

class OrderService:
    def __init__(self, repository: OrderRepository):
        self.repository = repository   # injected, not imported directly

    def get_order_total(self, order_id):
        return self.repository.get(order_id)["total"]
Layered architecture — one-way dependency, top to bottom

Presentation

HTTP views, CLI commands — talks to the service layer only

Service

Business use cases — talks to repositories through an interface

Repository

Data access — the only layer that imports a database driver

Database

PostgreSQL, SQLite, or any storage the repository wraps

  1. Presentation — HTTP views, CLI commands — talks to the service layer only
  2. Service — Business use cases — talks to repositories through an interface
  3. Repository — Data access — the only layer that imports a database driver
  4. Database — PostgreSQL, SQLite, or any storage the repository wraps

Comparing the four styles

Comparing the four styles
StyleUnit of separationTypical failure mode it prevents
LayeredHorizontal layers (presentation/service/data)A view function running raw SQL directly
Modular monolithVertical feature modules in one processEvery feature reaching into every other feature's tables
Clean architectureConcentric rings, business rules at the centerA domain rule that only works with one specific ORM
Hexagonal (ports & adapters)A core plus swappable adapters at its edgesBusiness logic that cannot be tested without a live database

Together

python
# order_service.py — the "service" layer; it never imports sqlite3 or Flask
class OrderRepository:
    def get(self, order_id):
        return {"id": order_id, "total": 42.50}


class OrderService:
    def __init__(self, repository):
        self.repository = repository          # depends on an abstraction, not sqlite3 directly

    def get_order_total(self, order_id):
        order = self.repository.get(order_id)
        return order["total"]


# presentation.py — the layer above; calls the service, never the repository directly
def get_order_total_view(order_id, service):
    total = service.get_order_total(order_id)
    return f"Order {order_id}: ${total:.2f}"

Remember: All four styles exist to stop business rules from depending on infrastructure — layered stacks that rule top-down, clean/hexagonal make it a hard boundary business code cannot cross.

See also: domain driven design fundamentals · repository · service layer

Domain-driven design fundamentals

standardintermediate

Domain-driven design (DDD) builds the code around the business's own concepts and rules, using the same words the business uses. An Order class enforces "a quantity must be positive" itself, instead of trusting every caller to check it first.

Think of it as

Without DDD, business rules scatter across views, scripts, and database triggers — the same "quantity must be positive" check gets copy-pasted, and drifts, in three places. DDD puts that rule inside the Order object itself, so every path that changes an order is forced through the same check. A value object (like Money) is defined entirely by its data and is immutable; an entity (like Order) has a persistent identity that survives changes; an aggregate is a cluster of entities and value objects with one designated entry point — the "aggregate root" — through which every change must pass.

python
class Money:                      # value object — no identity, immutable
    def __init__(self, amount, currency="USD"):
        self.amount, self.currency = amount, currency

class Order:                      # entity — has identity (order_id), and owns the invariant
    def __init__(self, order_id):
        self.order_id = order_id
        self._lines = []

    def add_line(self, product_id, quantity, unit_price):
        if quantity <= 0:
            raise ValueError("quantity must be positive")   # rule lives here, not at every call site
        self._lines.append((product_id, quantity, unit_price))

Remember: DDD models code around the business's own language and rules — a value object has no identity, an entity does, and an aggregate root is the only door into a cluster of both.

See also: layered modular monolith clean hexagonal · repository

Microservices and event-driven architecture

standardintermediate

Microservices split one system into independently deployable services, each owning its own data. Event-driven architecture is a way services in that split can communicate — publishing an event instead of calling each other directly.

Think of it as

A monolith is one process, one deployment, one database. Microservices split that into several processes — each with its own database — that communicate over the network, so a team can deploy the "billing" service without redeploying "shipping". Event-driven architecture changes HOW they communicate: instead of the order service directly calling the email service and the inventory service (tight coupling, one failure blocks the rest), it publishes an "order_placed" event to a bus, and any number of services subscribe without the publisher knowing who they are.

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):
        for handler in self._handlers.get(event_name, []):
            handler(payload)          # each subscriber reacts independently

What we're doing: Publish one event from an "order service" and have two independent subscribers react to it without the publisher knowing they exist.

event_bus_demo.pypython
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):
        for handler in self._handlers.get(event_name, []):
            handler(payload)


def send_confirmation_email(payload):
    print(f"[email-service] confirmation sent for order {payload['order_id']}")


def update_inventory(payload):
    print(f"[inventory-service] stock decremented for order {payload['order_id']}")


bus = EventBus()
bus.subscribe("order_placed", send_confirmation_email)
bus.subscribe("order_placed", update_inventory)
bus.publish("order_placed", {"order_id": 501})
18
Two unrelated services subscribe to the same event name — neither knows the other exists.
20
publish() only knows the event name and payload; it never calls send_confirmation_email or update_inventory directly.
Output
[email-service] confirmation sent for order 501
[inventory-service] stock decremented for order 501

Why this works: The order-placing code depends only on the bus, not on the email or inventory service — a new subscriber can be added later with zero changes to the publisher, which is the coupling event-driven architecture is trading for.

Remember: Microservices split a system by independent deployment and data ownership; event-driven architecture is one way those services talk — publish an event, let any number of subscribers react, without the publisher knowing who they are.

See also: observer · layered modular monolith clean hexagonal

Advertisement