Build a permit review service with two reviewers and disjoint assignments. Reviewer 47 may inspect and approve case 62; reviewer 81 may not learn its private title by guessing the URL. The queue shows at most 47 cases per page with district and attachment summaries, ordered by creation time and ID. The approval form uses CSRF protection and a stable operation ID. One submitted command changes state once, writes an audit operation and durable notification intent in the same transaction, and survives a lost HTTP response. A separate async read endpoint may await an external jurisdiction status, but its permission-scoped database work must return fully materialized values from a synchronous service. Keep the write transaction inside one synchronous function.
Project: Django permit review service
Build contract
- Order session and authentication middleware, then authorize the exact case in every read and command service.
- Bound the queue to 47 parents, join only its district, prefetch only displayed attachments, and test deep continuation.
- Reject missing CSRF tokens and invalid operation IDs; use a database uniqueness rule for command replay.
- Commit the approval and outbox event together, then wake delivery after commit without treating a callback as durable storage.
- Bridge synchronous ORM evaluation once from the async read view and apply a deadline to its external request.
Implementation checkpoint
function mayCommitApproval(commandId, committedIds) {
return !committedIds.has(commandId);
}
const committedIds = new Set(["case-62-reviewer-47-command-81"]);
console.log(mayCommitApproval("case-62-reviewer-47-command-81", committedIds));
// Output: falseCost and boundaries
A permission-scoped lookup adds database work to each private route, and that work must occur before a protected effect. A bounded queue uses O(page size plus fetched attachments) application memory; prefetching an entire unbounded result set may remove repeated queries while exhausting memory. A high offset can still force the database to scan earlier rows, so an indexed cursor is a better fit for long feeds. Approval locks can queue when reviewers race on one case; keep remote calls outside the transaction. Retaining command IDs and outbox events uses storage proportional to the chosen replay and delivery windows. An async view still needs thread capacity for its synchronous ORM bridge and a finite budget for external I/O.
Failure drill
Try anonymous access, a guessed case URL, and a valid reviewer with the wrong assignment. Render 47 queue rows and count SQL statements; remove the prefetch in a negative test to expose per-row queries. Submit the approval form without a CSRF token, then race two submissions with one operation ID. Drop the successful response and replay the same ID: the case status, audit record, and outbox intent must still exist once. Roll back a transaction and confirm no notification is sent. Delay the external status endpoint and enforce its deadline. Return a lazy QuerySet from a bridged service in a negative test and show why the response serializer must not evaluate it later.
Acceptance checks
- Every private read and write verifies the current reviewer-to-case mapping.
- Queue cost remains bounded, deterministic, and measured on a realistic data set.
- A duplicate command returns its existing outcome without another transition or event.
- The async endpoint performs no lazy synchronous ORM work on the event loop.
Common Mistakes
- Checking login while skipping case permission.
- Prefetching every attachment before applying a page boundary.
- Sending notification before commit or relying only on an in-process callback.
- Splitting a transaction across several async ORM bridges.
Related lessons
Django Request and Persistence Boundaries; Django Middleware, Sessions, and Object Permissions; Django QuerySet Shape, Prefetch, and Page Cost; Django Forms, CSRF, Atomic Approval, and On-Commit Work; Django ASGI, Async Views, and the Sync ORM Boundary.
