Build a Flask permit service around reviewer 47 and case 62. Reviewer 47 may see and approve the case; reviewer 81 must not discover its private note by guessing an ID. A detail route projects only public fields after an assignment-scoped query. A queue returns at most 47 cases with district labels and attachment totals, with timestamp and ID ordering. An approval command carries an operation ID, expected revision, and bounded reason. Browser CSRF checking, JSON shape validation, current object permission, and a short database transaction have separate jobs. The transaction stores one case transition, operation result, and outbox event. A lost HTTP response can be replayed under the same ID. A worker with its own application context claims the outbox row under a lease; a sweep finds it even when the queue hint is lost.
Project: Flask permit service boundaries
Build contract
- Use request context only at the HTTP edge and pass an explicit reviewer ID to services.
- Scope every private read in SQL and project only the fields shown to this reviewer.
- Bound each queue page, count attachments in SQL, and use a unique ordering tie breaker.
- Reject malformed commands, stale revisions, forbidden cases, and changed-payload replay.
- Commit the approval and outbox row together; recover delivery independently of the queue hint.
Implementation checkpoint
def may_claim_event(event_id: str, delivered_ids: set[str], leased_ids: set[str]) -> bool:
return event_id not in delivered_ids and event_id not in leased_ids
delivered_ids = {"permit-62-approved-47"}
leased_ids = {"permit-81-reviewed-29"}
print(may_claim_event("permit-62-approved-47", delivered_ids, leased_ids))Cost and boundaries
The detail route pays for an assignment predicate as well as the case lookup. A 47-row queue remains bounded only when scope and limit are in SQL; filtering after loading the full table transfers private rows and increases memory. Attachment counts cost database work but avoid loading every attachment model. An approval lock serializes conflicting writes for one permit, while unique operation records consume storage over the replay horizon. The outbox adds a write and a worker sweep, yet leaves an inspectable record when a process fails. A Flask async view still occupies a WSGI worker, so use it for concurrent awaited I/O within one request, not as a substitute for durable delivery.
Failure drill
Guess case 62 as reviewer 81 through detail, export, and download paths. Add a private_note fixture and assert that no response includes it. Seed 47 cases with equal created times and follow all cursor pages without duplicate IDs. Give one case 620 attachments and inspect whether the list hydrates any attachment models. Submit malformed JSON, a bool revision, a missing CSRF token, and an expired assignment. Race two requests with the same operation ID. Commit an approval, discard the response, then retry the exact command and inspect one case transition and one outbox row. Roll back before commit, kill the process after commit but before queue submission, expire a worker lease, and timeout the provider after possible acceptance.
Acceptance checks
- A valid session never grants another reviewer's permit access.
- Queue memory and row counts stay bounded while timestamp ties traverse deterministically.
- An exact replay returns the stored result; changed input under the same ID conflicts.
- A committed outbox event is recoverable after a lost wake-up or worker restart.
Common Mistakes
- Passing Flask context proxies into services or workers.
- Serializing the full ORM model after an access check.
- Using process memory as the approval replay ledger.
- Treating an async child task or queue hint as durable intent.
Related lessons
Flask Context and Service Boundaries; Flask Request Context and Object Authorization; Flask-SQLAlchemy Session and Query Lifetime; Flask Command Validation and Transaction Replay; Flask Async Views and Durable Background Work; Flask routing: application factories, converters and test clients; Python Flask keyset list: validate a cursor before selecting the next page.
