Build a Rails permit workflow for reviewer 47, reviewer 81, and case 62. Reviewer 47 may inspect and approve the case; reviewer 81 cannot discover its private district note by guessing an ID. The controller derives identity from the server session, admits only the approval command fields, and uses a reviewer-scoped relation for private reads. The queue shows at most 47 cases with district labels and attachment totals, ordered by creation time and ID. An approval request carries operation ID, expected revision, and a bounded reason. A short transaction locks current case state, checks current assignment, and commits one status change, one operation result, and one outbox event. Exact retry after a lost response returns that result; changed input under the same ID conflicts. An Active Job wake-up waits for commit, while a sweep finds a pending event if queue handoff was lost.
Project: Rails permit review workflow
Build contract
- Use authentication, browser authenticity-token checks, strong parameters, and exact case permission as separate guards.
- Scope the queue before limiting; preload displayed district data and avoid materializing attachment collections for counts.
- Lock current case state, check assignment and revision, and commit a unique operation outcome with the outbox row.
- Defer queue wake-up until commit and recover pending outbox rows through an independent bounded sweep.
- Project private responses narrowly and map missing, forbidden, stale, replay, and internal failures deliberately.
Implementation checkpoint
def claimable?(event_id, delivered_ids, leased_ids)
!delivered_ids.include?(event_id) && !leased_ids.include?(event_id)
end
delivered_ids = ["permit-62-approved-47"]
leased_ids = ["permit-81-reviewed-29"]
puts claimable?("permit-62-approved-47", delivered_ids, leased_ids)Cost and boundaries
A reviewer-scoped detail lookup pays for the assignment predicate, but avoids returning private data before permission is established. The queue should remain O(47) in parent objects for one page. Eager loading the displayed district prevents repeated queries; eager loading every attachment can still consume memory proportional to all child records. A keyset cursor reduces deep-offset scanning when the filter and order have suitable indexes, while exact numbered pages require a different cost choice. A row lock serializes competing approvals for case 62. Unique operation and outbox rows add writes and retained storage, but make lost responses and queue handoff failures inspectable. An after-commit enqueue prevents uncommitted reads; it does not replace durable intent when the queue uses separate storage.
Failure drill
Guess case 62 as reviewer 81 through show and export routes. Submit extra status and reviewer_id fields with a valid authenticity token, then submit a valid command with no token. Give the queue 47 timestamp ties and one case with 620 attachments; inspect query count, object allocations, and continuation order. Revoke reviewer 47's assignment after loading the page and attempt approval. Race two requests under the same operation ID, lose one HTTP response, and replay. Change the reason under that same ID and require a conflict. Raise after case update but before outbox insertion, then inspect rollback. Commit and kill the web process before enqueue; the sweep must later find the event. Expire a worker lease and retry a provider timeout under the same external event key.
Acceptance checks
- A valid login does not grant another reviewer's case access or permit status mass assignment.
- The bounded queue has no district N+1 queries and no unbounded attachment hydration.
- One operation ID creates one committed result and one outbox event across concurrent exact retries.
- A lost queue hint cannot erase notification intent committed with the approval.
Common Mistakes
- Using strong parameters as if they authorize a record.
- Fixing N+1 by loading all children for every list page.
- Sending provider email from the case transaction.
- Assuming post-commit enqueue is a durable outbox.
Related lessons
Rails Request, Record, and Job Boundaries; Rails Controller Parameters and Object Permission; Rails Active Record Scope, Eager Load, and Page Cost; Rails Locking, Transaction, and Command Replay; Rails Active Job After-Commit and Outbox Recovery.
