MyCase Immigration / Docketwise Integration: 0: Why We Synced · 1: Before/After · 2: OAuth2 · 3: Sync vs Migration · 4: Sync Boundary · 5: Family Graph · 6: Write Patterns · 7: Webhooks · 8: Billing
After we drew the sync boundary at Client and Case and partitioned source-of-truth by field domain, the write patterns were not an open question. They followed from the ownership decision.
This post covers what source-of-truth partitioning looks like for a single entity where both systems own different fields, the two write patterns that partitioning produced, and why the alternative for each was explicitly rejected.
Source-of-truth partitioning on a single entity
A MyCase Case record and a Docketwise case are the same immigration matter. They share a docketwise_case_id FK. They are not replicas. Each owns different fields.
Docketwise owns the immigration-specific fields: visa category, matter type, USCIS form associations, party role relationships, immigration timeline. When an attorney edits one of these fields in the MyCase UI, MyCase is writing to Docketwise's domain. The correct behavior: write to Docketwise, not to MyCase.
MyCase owns the practice management fields: assigned attorney, billing rate, deadline notes, document folder structure. When Docketwise needs to know which attorney is on a case, it reads from MyCase. The correct behavior: MyCase saves the field, pushes a copy to Docketwise.
Source-of-truth partitioning says: the field lives where it is owned, and writes go to the owner. There is no "save to both and reconcile later." There is no distributed transaction that writes to both atomically. The field has one owner, the write goes to the owner, the other system gets a copy when the owner sends one.
Write pattern 1: synchronous REST for immigration writes
When a MyCase user edits an immigration-owned field in the MyCase UI (changing a case status, updating a visa category, editing a party's name that Docketwise owns), the Rails controller makes a synchronous REST call to the Docketwise FastAPI endpoint in the same request cycle.
There is no local save first. MyCase does not write the field to its own database before confirming the Docketwise write succeeded. If Docketwise's API returns an error, the controller returns an error to the browser. The field change does not persist anywhere.
The sequence:
- Attorney submits form with immigration field change.
- Rails controller receives the request.
- Controller calls Docketwise REST API with the new field value.
- Docketwise validates and saves.
- Docketwise returns 200.
- Controller returns success to the browser.
If step 4 fails, step 6 returns an error. No partial save in MyCase.
Why async was rejected for immigration writes
The async alternative: MyCase saves the field locally, enqueues an outbox job, and the background worker pushes to Docketwise asynchronously. The user gets a success response immediately.
This creates an ordering conflict with incoming webhooks.
Docketwise also pushes updates to MyCase via webhooks (Part 7 covers this in detail). If an attorney makes an immigration write from MyCase and the async outbox is still queued when Docketwise sends a webhook for a different update to the same case, MyCase may apply the webhook state (which does not include the pending outbox change) on top of a local record that has an un-pushed field change. One of the two writes is now wrong.
The write queues would race: the outbox queue (MyCase pushing to Docketwise) and the webhook queue (Docketwise pushing to MyCase) both writing to the same record, with no ordering guarantee between them.
Synchronous REST eliminates this conflict. The moment the controller returns success, Docketwise has the new value and is now the canonical source. Any subsequent webhook from Docketwise carries state that is at least as current as the just-confirmed write. There is no pending local value waiting in an outbox.
Degradation when Docketwise is unavailable
Synchronous REST makes Docketwise a hard dependency for immigration field edits. If Docketwise is unavailable, the attorney cannot edit immigration fields in MyCase.
The circuit breaker behavior: MyCase tracks Docketwise API availability. When the circuit is open (Docketwise unreachable or erroring above threshold), the MyCase UI renders immigration fields as read-only with a user-visible message. The attorney can still work on practice management tasks in MyCase (billing, document management, internal notes). They cannot edit immigration fields until Docketwise is available.
This is the correct tradeoff: availability of immigration writes is bounded by Docketwise availability, because Docketwise owns those fields. Pretending otherwise by queuing writes locally would create the ordering conflicts described above, and it would mean the user sees a "success" response for a change that has not actually been saved to the system of record.
Write pattern 2: async outbox for metadata
When a MyCase user edits a MyCase-owned field (assigned attorney, billing rate, deadline notes), the write follows a different path: save to MyCase first, push to Docketwise asynchronously.
The sequence:
- Attorney submits form with metadata change.
- Rails controller receives the request.
- Controller opens a database transaction.
- Inside the transaction: save field to MyCase database, enqueue outbox job.
- Transaction commits.
- Controller returns success to the browser.
- Background worker picks up outbox job, calls Docketwise REST API.
- Docketwise stores the metadata copy.
If step 7 or 8 fails: the outbox job retries. Docketwise has a slightly stale copy of MyCase-owned metadata until the retry succeeds. This is acceptable because MyCase (the owner) has already confirmed the write in step 5. The stale copy in Docketwise is a display inconvenience, not a data integrity problem.
The three-step transaction in step 3-5 is the critical design. The outbox job is enqueued inside the same database transaction that saves the field. If the transaction rolls back, the outbox job is not enqueued. The local save and the outbox enqueue are atomic; there is no state where the local record was saved but no background push was queued.
Why sync was rejected for metadata writes
The sync alternative for metadata: MyCase writes to Docketwise in the request cycle before returning to the user.
This makes Docketwise availability a dependency for all metadata writes. An attorney who changes their billing rate on a case cannot save it if Docketwise is unreachable, even though billing rate is a MyCase-owned field with no immigration dependency.
Metadata writes are not immigration-critical. If Docketwise has a slightly stale copy of which attorney is assigned to a case, the immigration case work is not affected. The delay is seconds to minutes, bounded by retry frequency.
Making Docketwise a hard dependency for writes to fields it does not own would have made Docketwise degradation impact MyCase workflows that have no actual dependency on Docketwise. That is a worse tradeoff than eventual consistency on metadata.
The general principle
Transport is determined by ownership. If the system receiving the write is the source of truth for the field being written, the write must be synchronous: the write is not complete until the owner confirms it. If the system receiving the write is a consumer of a field owned elsewhere, the write can be async: the owner has already confirmed it, and the consumer will receive an eventually consistent copy.
The pattern pair follows from this principle directly. Synchronous REST is the correct mechanism when you are writing to a remote source of truth. Async outbox is the correct mechanism when you are pushing a copy to a system that is not the owner.
Both patterns exist in this integration because source-of-truth is split between two systems on the same records. Each pattern applies to the field domain that matches its ownership assumption. Swapping them would have introduced either ordering conflicts (async for immigration writes) or unnecessary availability coupling (sync for metadata writes).
Next: Part 7. Webhook Ordering, Version Guards, and the Idempotency Key. Part 6 covers what MyCase pushes to Docketwise. Part 7 covers what happens when Docketwise pushes back. The failure mode in staging was a webhook ordering problem that required two separate fixes.


Top comments (0)