Summary
Four layers, dependencies pointing downward only, and business logic that imports neither HTTP nor SQL. Enforce it with package layout and a lint rule rather than trusting review. A circular import is a design signal, and the fix is usually to move the shared thing into a lower layer.
One settings class covering every environment, with environment-specific rules in a validator, and a startup check that verifies every dependency is reachable and reports all failures at once. Secrets come from a manager or the platform, never a file in the image, and rotation is designed as an overlap before it is needed.
The API layer translates and nothing more, with its own request and response models separate from domain records. One error handler maps the Module 5 exception hierarchy to status codes and attaches the request identifier. Liveness never checks dependencies and readiness does. Graceful shutdown marks itself unready, waits for the load balancer to notice, drains in-flight work with a bounded timeout, and tells in-flight streams that their output is partial.
Work that outlives a request goes to a queue with an idempotency key, a status endpoint, and optionally a signed webhook carrying an identifier rather than data. Type checking strict, Protocols as enforced contracts, and the same lint and format rules in CI as in the hooks.
Security review is a checklist over boundaries you already validate, plus never deserializing untrusted data with a format that constructs objects, auditing dependencies on a schedule, and the rule that matters most: an agent's permissions must never exceed the user's, scoped inside the query rather than filtered afterwards.
One image built once, promoted through environments with configuration as the only difference, rolled out gradually while watching error rate, latency, and spend. Roll back before diagnosing. Model and prompt changes go behind flags so they can be reverted in seconds, and flags are removed once their rollout completes. Docstrings record decisions, ADRs record the alternatives you rejected, and the README is tested on someone who has never seen the project.
Key takeaways
- Dependencies point toward what changes least often
- The domain layer imports nothing of yours, which is the test that the layering is real
- Enforce architecture with a lint rule, not with reviewer attention
- Validate every dependency at startup and report all failures at once
- Liveness must never check a dependency, or a database blip becomes a restart storm
- The drain delay before shutdown is what stops requests being dropped on every deploy
- An in-flight stream that is cut must be told it was cut
- Background work needs an identifier, a status endpoint, and an idempotency key
- The model's permissions must never exceed the user's
- Scope data access inside the query, not after it
- Build one artifact and change only configuration between environments
- Roll back first, diagnose after
- Flags revert in seconds where a deployment takes minutes
- A prompt or model change is a breaking change even though no signature changed
- The README is tested on a person, not proofread
Common mistakes to remember
- A service module importing a database driver or an HTTP client
- Fixing a circular import by moving it inside a function and leaving the design alone
- Building dependencies per request instead of at the composition root
- Reusing domain records as API response models
- Translating errors in every route handler
- A liveness probe that checks the database
- Shutting down without a drain delay
- Terminating in-flight streams silently
- Using in-process background tasks for work whose completion matters
- Returning 200 for work that has not happened yet
- Letting a webhook payload carry the result instead of an identifier
- Authenticating an endpoint and forgetting to authorize it
- Filtering by permission after retrieval instead of inside the query
- Deserializing untrusted input with pickle or an unsafe YAML loader
- Rebuilding the image per environment
- Deploying without a rehearsed rollback
- Accumulating flags and never removing them
- A README that assumes knowledge only the author has