Application Architecture and Operations¶
Layout¶
Use domain names instead of generic utils or helpers modules:
src/invoicing/
├── interfaces/http.py
├── application/create_invoice.py
├── domain/invoices.py
└── infrastructure/invoice_repository.py
tests/
├── unit/application/test_create_invoice.py
├── unit/domain/test_invoices.py
├── integration/infrastructure/test_invoice_repository.py
└── functional/test_create_invoice.py
Boundaries¶
- Let interfaces translate transport input and output.
- Let application functions orchestrate one use case.
- Keep reusable business behavior in domain modules or domain objects.
- Keep infrastructure access visible and controllable at the unit boundary.
- Follow the project's chosen Active Record or Data Mapper model consistently.
- Validate user-controlled payloads in interfaces with Pydantic. Pass trusted values inward without repeating boilerplate validation.
- Separate read queries from state-changing operations only when it improves discovery.
- Serialize rich objects before template rendering when attribute access could trigger I/O or business behavior.
Example¶
Place CreateInvoiceInput in interfaces/http.py, the domain objects in
domain/invoices.py, and create_invoice in the application layer.
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, Field
class CreateInvoiceInput(BaseModel):
account_id: str = Field(min_length=1)
amount: Decimal = Field(gt=0)
@dataclass(frozen=True)
class Invoice:
account_id: str
amount: Decimal
issued_at: datetime
def create_invoice(
*,
account_id: str,
amount: Decimal,
issued_at: datetime,
save_invoice: Callable[[Invoice], None],
) -> Invoice:
invoice = Invoice(
account_id=account_id,
amount=amount,
issued_at=issued_at,
)
save_invoice(invoice)
return invoice
The interface validates the payload and owns the clock, then passes trusted
values inward. The infrastructure adapter supplies save_invoice.
Operations¶
- Distinguish anticipated business failures from unanticipated failures.
- Log a stable action-oriented message with
logger.exceptionat the boundary that can handle or report the failure. - Use stable event names, IDs, and ISO-formatted timestamps.
- Treat background-job payloads as deployed contracts. Use additive fields or a two-phase rollout when old and new workers may coexist.
- Model periods with an inclusive lower bound and exclusive upper bound.