DEV Community

Cover image for Why We Synced Instead of Migrating: Bundling an Acquired Product Without a Big-Bang Rewrite
Son Do
Son Do

Posted on • Edited on

Why We Synced Instead of Migrating: Bundling an Acquired Product Without a Big-Bang Rewrite

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

Acquire a company, and the obvious move is to migrate its customers onto
your platform. That's the standard playbook. It's not what happened here.

MyCase had acquired Docketwise, an immigration practice-management SaaS, and
the plan was to bundle it into MyCase as a native Immigration addon. We
started where everyone starts: with a migration plan on the whiteboard,
move every standalone Docketwise customer's data into MyCase, retire the old
system, done. Then someone asked the question that killed it: what happens
to a Docketwise customer who hasn't converted yet but edits a case the week
after we run the migration? Their live system and their migrated copy would
disagree, and nobody had a good answer. The constraint underneath that
question was blunt: existing standalone Docketwise customers had to keep
working, correctly, indefinitely, whether or not they ever fully converted
to the bundled addon. Not for a migration window. As a permanent state.

We tech-led the 2-4 engineer effort that built what shipped instead: a
permanent bidirectional sync between two independently owned systems, on
two unfamiliar stacks, MyCase's Rails and React against Docketwise's
FastAPI and Vue. This post is about why migration couldn't satisfy the
constraint it was supposed to solve, and how we decided exactly how much of
each system actually needed to talk to the other.

Why a one-time migration doesn't actually solve the problem

The business reality was blunt: customers wouldn't convert to the bundled
MyCase Immigration addon unless their data stayed correct throughout the
transition. That sounds like an argument for a careful migration. It's
actually an argument against one.

The initial project scope assumed migration. We spent the first week mapping
what a one-time cutover would require before the staleness problem became
obvious. A migration is a point-in-time snapshot. It copies data as of the
moment it runs, and then it's done. The instant a Docketwise-side user
touches a record after that snapshot, MyCase's copy goes stale. Nothing
about the migration having gone well the first time prevents that. If the
constraint is "data must stay correct throughout," a snapshot only satisfies
it up to the moment it's taken, and standalone Docketwise customers were, by
definition, still going to use Docketwise.

Satisfying the real constraint means keeping both systems correct for as
long as a customer might touch either one, which means the systems need a
permanent sync path regardless of whether a migration also happens. And
once that sync path exists, a one-time migration on top of it is redundant
work. It solves a staleness problem that permanent sync already solves
better.

One-time migration goes stale the instant a Docketwise user edits a record after the migration runs; permanent sync keeps both systems correct indefinitely

Finding the smallest thing that actually needs to sync

Docketwise and MyCase model a legal case differently enough that merging
their schemas outright wasn't a serious option. The real design question
was narrower and harder: how much of each system's data actually needs to
be visible to the other.

The instinct to reach for is "sync everything useful," and almost
everything in a legal case is useful to somebody. That's the wrong test.
The test that actually narrows the field is: does a workflow in one system
need to reference a record that lives in the other. Most of what each
system stores, documents, tasks, billing detail, communication history,
never crosses that line. Nothing in MyCase needs to look up a Docketwise
document directly, and nothing in Docketwise needs to look up a MyCase
task. Two things do cross it: who the client is, and what case or matter
their work belongs to. Every cross-system workflow, billing an immigration
matter through MyCase, opening a case file that references a client MyCase
already knows about, ultimately needs both systems to agree on Client and
Case identity, or on nothing at all.

Sync architecture: Docketwise and MyCase as two independent systems, each keeping its own full schema, connected by a thin bidirectional sync bridge on Client and Case records only

We drew the boundary there: Client and Case sync, nothing else. That test
kept the sync surface small. Client and Case were the only two entities that
crossed the boundary; everything else stayed inside each system's own full
schema, and neither side had to model concepts it didn't already understand.
The alternative, syncing broadly because the data might someday be useful
somewhere, would have meant maintaining consistency across a much larger
surface for cross-system workflows that, in most cases, didn't exist yet and
might never.

Even within those two synced entities, source-of-truth was partitioned by
field domain. Docketwise owns immigration-specific fields; MyCase owns
practice management fields. Writes go to the owner. The rest of the series
covers what that means for write paths, webhook ordering, and schema
resolution.

The same discipline applied to a different problem

A second constraint showed up on the billing side. MyCase's addon-billing
logic, trial period, conversion, proration, cancellation, had been
hardcoded to a single existing addon, and the Immigration addon needed the
same workflow without a rebuild. At the same time, Docketwise's standalone
customers were already on Stripe for billing, while MyCase ran its own
subscription billing on Zuora, and neither could be disrupted mid-bundle
without putting a real customer's trial or payment at risk.

We chose not to touch either provider integration directly. The fix was to
generalize the trial/conversion/proration workflow so it wasn't
addon-specific, leaving both live integrations underneath it untouched. The
new Immigration addon could launch on the same billing workflow every other
addon used, while Stripe kept charging existing Docketwise customers and
Zuora kept charging MyCase's, both running untouched through the transition.
When a Docketwise standalone customer converted to the bundled addon, their
Stripe subscription was soft-deactivated rather than deleted, preserving the
reversion path if they later canceled and returned to standalone.

What shipped

MyCase Immigration, powered by Docketwise, launched publicly on October 1,
2024, and reached general availability in 2025. The part that stayed with us
was the quiet version of success: existing standalone Docketwise customers
converting to the bundled addon without anyone running a data migration, the
sync having been keeping Client and Case correct on both sides the whole
time in the background. Customers kept working through the entire transition
on Docketwise's original stack, and billing kept running on both providers
without a disruption to a single customer's trial or payment.

The lesson that generalizes past this one integration: when someone
proposes migrating data between two systems because a merger or acquisition
demands it, the sharper question is what actually needs to be true across
both systems while people keep using them. Usually the answer is a lot
smaller than "everything," and finding that boundary is worth more than
executing the migration well.


Next: Part 1. Before/After: What "MyCase Immigration, Powered by Docketwise" Took to Ship. The structural overview of both stacks, the sync boundary, and a map of what the following posts cover in depth.

Top comments (0)