Filter concepts by levelShowing all levels.

Python · Architecture styles

Common architecture styles

Layered, modular monolith, clean, and hexagonal architecture, domain-driven design, and microservices/event-driven architecture — where to draw the boundary around business logic

  • Layeredpresentation -> service -> repository -> database, one-way
  • Clean/hexagonalevery outer layer depends inward on the business rules
  • Domain-driven designvalue objects, entities, and one aggregate root per cluster
  • Event-drivenpublish an event, let any number of subscribers react

1.Layered, modular monolith, clean, and hexagonal architecture

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.

Key features

  • Layered: presentation → service → repository → database, each layer calling only the one directly below it
  • Modular monolith: one deployable process, split into feature modules with enforced boundaries between them
  • Clean/hexagonal: business rules depend on nothing; infrastructure (DB, web, CLI) depends inward on the business rules through interfaces
  • Hexagonal names the same idea "ports and adapters" — a port is an interface the core defines, an adapter is the concrete implementation plugged in
  • None of the four dictate a specific framework — they dictate which direction an import is allowed to point

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

Command

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"]

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.

2.Domain-driven design fundamentals

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.

Key features

  • Ubiquitous language: code and business conversation use the exact same terms — "Order", not "Record" or "Row"
  • Value object: defined only by its data, no identity — two Money(10, "USD") are interchangeable, and it is immutable
  • Entity: has identity that persists across changes — the same Order stays "the same order" after its total changes
  • Aggregate: a cluster of entities/value objects with one root; outside code may only reach it through the root
  • Bounded context: a model is only valid within one explicit boundary — "Customer" can mean different things in Billing vs. Support

Command

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))

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.

3.Microservices and event-driven architecture

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.

Key features

  • Microservices: independently deployable services, each owning its own data store — no shared database
  • The cost of microservices is operational: network calls replace function calls, and failures/latency become distributed problems
  • Event-driven: a publisher emits an event without knowing which services subscribe to it, or how many
  • Events decouple services in time as well as identity — a subscriber can be down when the event is published and catch up later, if the bus persists it
  • Choosing microservices/event-driven for a small team or low scale often adds coordination cost with no matching benefit — a modular monolith is the usual middle ground

Command

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

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.

Remember

  • All four structuring styles exist to stop business rules depending on infrastructure — layered stacks that rule top-down, clean/hexagonal make it a hard inward-only boundary.
  • DDD models code around the business's own language — a value object has no identity, an entity does, an aggregate root is the only door into a cluster of both.
  • Microservices split by independent deployment and data ownership; event-driven is one way those services talk without knowing about each other.

Pro tip

  • A modular monolith is the usual middle ground before microservices — feature boundaries without the network/deployment cost.
  • Choosing microservices for a small team or low scale often adds coordination cost with no matching benefit.

Next up

  • Patterns

Sources

  • Martin Fowler — martinfowler.com/architecture
  • Illustrative snippets run for real on Python 3.14.3 · checked 2026-08-22

Spotted a mistake on this page? Tell us.