DEV Community

Cover image for The Family Graph Problem: When Schemas Don't Just Differ in Fields
Son Do
Son Do

Posted on

The Family Graph Problem: When Schemas Don't Just Differ in Fields

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

When we drew the sync boundary at Client and Case, the next question seemed simple: what fields do we map?

For Case fields, it was simple. Case name, status, assigned attorney: all 1:1 mappings with minor string transformations.

For Client fields, the mapping exercise stopped immediately. Docketwise does not have a single Client concept equivalent to MyCase's Contact record. It has a typed family graph.

This post covers what that means structurally, why it cannot be resolved by field mapping, and how we resolved it.


What Docketwise actually stores

In immigration law, a case typically involves multiple parties with typed roles. An employment-based case has a petitioner (the sponsoring employer) and a beneficiary (the employee being sponsored). A family-based case has a petitioner (the sponsoring family member) and a beneficiary plus their derivative beneficiaries: a spouse and any children under 21.

Docketwise models this as a typed family graph. A case links to multiple parties. Each party has a type attribute: petitioner, principal beneficiary, derivative beneficiary. Each party record also carries typed relationship references: a derivative beneficiary record references the principal beneficiary it is associated with.

This graph is required for immigration-specific functionality. USCIS Form I-130 is filed by the petitioner on behalf of the beneficiary. Form I-485 is filed by the beneficiary. Derivative beneficiaries file Form I-485 separately. The system needs to know who is who to know which forms apply and how to fill them.

The graph also carries immigration-specific identity fields: passport number, country of birth, date of entry, visa classification. These are distinct from the identity fields MyCase uses for billing: name, email, phone.


What MyCase actually stores

MyCase's Contact record is a flat entity. Each client is one record: name, email, phone, billing configuration, and portal access. There is no concept of party role, no typed relationship between clients, and no immigration-specific identity fields.

A MyCase matter links to one primary contact. Billing goes to that contact. The matter folder is associated with that contact. Communications go to that contact.

There is no native way to represent "this matter has a petitioner, a beneficiary, and two derivative beneficiaries, and the derivative beneficiaries are associated with the principal beneficiary."


Why field mapping does not resolve this

The naive approach: add a party_role field to the MyCase Contact record. Set it to "petitioner" or "beneficiary" when syncing from Docketwise. Done.

This breaks on the relationship structure. A MyCase Contact record with party_role: "beneficiary" does not know which petitioner it is associated with. There is no FK between Contact records in MyCase. A matter with two derivative beneficiaries and one principal beneficiary would require three Contact records that know about each other, and MyCase's Contact model has no native mechanism for that.

It also breaks on the matter linkage. A MyCase matter has a primary contact. An immigration case in Docketwise has multiple parties. Mapping an immigration case to a MyCase matter requires either picking one party as the "primary" (which loses the others) or linking multiple contacts to the same matter (which MyCase's billing model does not natively support in the way Docketwise's case model requires).

This is a structural incompatibility, not a field-level one. The unit of "one client per matter" in MyCase and "multiple typed parties per case" in Docketwise are different shapes that cannot be collapsed into each other by adding fields.


The resolution

The resolution: one MyCase Contact record per Docketwise party. Each Contact stores the typed party role and the inter-party relationship FKs as metadata. The MyCase matter links to all participating Contacts. The matter carries a docketwise_case_id FK.

Docketwise typed family graph on left with case node, petitioner, principal beneficiary, and two derivative beneficiary nodes with typed edges; MyCase structure on right with one matter linked to four Contact records each carrying party_role, docketwise_party_id, and optional related_party_id metadata

The Contact metadata fields added for this:

  • docketwise_party_id: the Docketwise party record UUID, used as the sync key
  • party_role: the typed role string from Docketwise (petitioner, principal_beneficiary, derivative_beneficiary)
  • related_party_id: FK to another MyCase Contact record, pointing to the principal beneficiary for derivative beneficiaries; null for petitioners and principal beneficiaries

These fields are not MyCase-native concepts. They are Docketwise-sourced metadata stored on the Contact to allow the MyCase immigration UI to render the party structure correctly.

The matter-to-contact linkage: the existing matter-to-primary-contact relation is preserved. The additional party contacts are linked to the matter via a join table that carries the docketwise_party_id and party_role. MyCase billing still works against the primary contact; the immigration UI reads all party contacts from the join table.


What this does not sync

The resolution stores enough of the Docketwise party graph in MyCase for correct immigration UI rendering. It does not attempt to sync the full immigration data model.

These stay in Docketwise and are never stored in MyCase:

  • Passport numbers, dates of entry, visa classifications
  • USCIS form associations and form-specific field values
  • Immigration timeline entries and USCIS response records
  • Matter type and visa category

When the MyCase immigration UI needs to display these fields, it fetches them from the Docketwise API on demand. MyCase does not cache them locally. This was a deliberate choice: avoid creating a local copy that could diverge from Docketwise's authoritative record.

The sync covers what both systems need for their core workflows. The display of immigration-specific fields is not a sync problem; it is a real-time read problem. Those are different, and they have different failure modes.


The null-handling decision

Nullable handling came up during the family graph mapping because Docketwise occasionally sends partial party records. A derivative beneficiary added at case creation sometimes lacks a date of birth (not yet filled in by the attorney) or a passport number. Docketwise sends these fields as null.

The initial instinct was to reject syncs for incomplete records. But rejection would leave the MyCase Contact record in an unsynced state indefinitely if Docketwise did not resend the record after the data was completed. In practice, attorneys fill in party details over time, not all at once at case creation.

The decision matches the null-handling policy established in Part 4: MyCase stores null for Docketwise-owned fields it receives as null. It logs a warning at the field level so missing data is observable. It does not reject the sync. A partial Contact record in MyCase is better than no Contact record, because an attorney can at least see the party exists and navigate to Docketwise to fill in the missing fields.

Validation for Docketwise-owned fields is Docketwise's responsibility. MyCase stores what it receives.

Null handling decision: when Docketwise sends a partial party record with null fields, MyCase stores null and logs a field-level warning rather than rejecting the sync; rejection would leave the Contact record permanently stale if Docketwise never resends


Next: Part 6. Two Write Patterns from One Architectural Decision. The family graph tells us what syncs. Part 6 covers how writes happen: why two entities in the same sync boundary ended up with two different write mechanisms, and why that followed directly from source-of-truth partitioning.

Top comments (0)