How to separate HTTP routes, application use cases, domain rules and infrastructure without turning a Python backend into ceremonial layers.

Production principle

Put orchestration at the application boundary, keep business decisions independent of transport and make infrastructure dependencies point inward through explicit contracts.

Degraded
@router.post("/orders")
def create_order(data: dict):
    customer = db.query(Customer).get(data["customer_id"])
    total = calculate_total(data)
    order = Order(customer=customer, total=total)
    db.add(order)
    db.commit()
    email.send(customer.email)
    return order
Repaired
@router.post("/orders")
def create_order(command: CreateOrder, service: OrderService):
    return service.create(command)

class OrderService:
    def create(self, command: CreateOrder) -> OrderResult:
        with self.uow as uow:
            order = Order.create(command, uow.customers)
            uow.orders.add(order)
            uow.events.add(OrderCreated(order.id))
            uow.commit()
        return OrderResult.from_order(order)
01

Begin with responsibilities

A route is responsible for the transport contract: parsing input, invoking an application operation and translating its result into HTTP. When it also decides pricing, opens transactions and sends notifications, every concern becomes coupled to the framework and to the request lifecycle.

An application service coordinates one use case. It loads the required state, asks domain objects to make decisions, persists the result and records work that must happen after commit. This boundary gives transaction ownership and error behaviour one visible home.

  • Routes translate protocols; they do not own business rules.
  • Domain objects protect invariants without importing FastAPI or SQLAlchemy sessions.
  • Infrastructure implements explicit ports for persistence and external communication.
02

Avoid layers without decisions

A service that merely forwards every argument to a repository adds indirection without creating a boundary. A repository that exposes every database method gives the application a second ORM rather than a domain-focused persistence contract.

Introduce an abstraction when it isolates volatility, owns a policy or creates a test seam that represents a real boundary. Keep simple read models direct when no business decision is involved; not every query needs to travel through the same architecture.

03

Make dependencies visible

Construct application services at the composition root and pass their dependencies explicitly. Hidden global clients and sessions make lifetime, configuration and test replacement ambiguous.

Architecture evidence includes import direction, transaction ownership and tests that execute use cases without an HTTP server. The objective is a change surface: replacing the carrier client should not modify order rules, and changing HTTP representation should not modify persistence.

Review checklist

Evidence to take into review

  • Route handlers contain transport concerns only.
  • One application boundary owns each business transaction.
  • Domain code imports no web framework or database session.
  • Infrastructure dependencies are explicit at construction time.
  • Every abstraction protects a real decision or volatile boundary.
Continue the inspection

Explore all engineering notes.

Use PRODUCTION-7 to connect this concern with the other dimensions of a trustworthy backend.

View all articles Get the checklist