We are moving a feature from MongoDB to SQL. One team owns the Mongo side. A different team owns the SQL side. Both want the same common package and the same Function App, with as little code change as possible, and a simple toggle to switch between them.
The obvious design is an if (useSql) somewhere. The question is where it lives. I think most people put it in the wrong place.
The tempting spot
The tempting spot is inside the shared package. One OrderService, one repository class, and a flag that picks the Mongo driver or the SQL client halfway down the call. It feels minimal. One file changes.
It is also the version where two teams edit the same class. The Mongo team knows nothing about your SQL joins. The SQL team has never seen your aggregation pipeline. Within a month the flag has leaked into three methods, the tests need both databases running, and nobody can delete the Mongo branch because nobody is sure what it still covers.
A toggle inside shared code means the shared code now has two owners and no owner.
Where I'd put it
Put the toggle at the seam. Define a narrow contract, an interface with maybe four or five methods, that describes what the feature needs from storage in business terms: GetByCustomer, Save, MarkProcessed. Not Find(filter). Not IQueryable. Anything that smells like a Mongo filter or a SQL predicate stays out.
Then two implementations. Each team owns one, in its own assembly or its own module. The shared package ships the contract and a single factory or DI registration. That registration is the only place the flag is read.
The Function App never sees the flag. It asks for the contract and gets whatever was wired at startup.
Small change. Big difference in who can break what.
The part that bites: the contract is the real migration
The contract is where the work hides. Mongo lets you store a nested document and read it back whole. SQL wants that split across tables. If your contract returns the nested shape, the SQL team has to reassemble it on every read, and they will resent it. If you design the contract around the relational shape, the Mongo team has to flatten.
Neither is free. I'd lean toward designing it around the domain object the Function App actually consumes, and making the SQL team do the reassembly once, in one mapper, with a test. That puts the cost on the side that is the destination. Mongo is the thing going away, so it shouldn't dictate the shape of the thing that stays.
There is a counterpoint, and it's a real one: a contract that fits both stores tends to be the lowest common denominator, and you lose whatever made one of them fast. If one query path is performance-critical, give it its own method on the contract and let each side optimise it independently. Don't hide it behind a generic one.
Toggle granularity: per deployment beats per request
Once the seam exists, the next temptation is a runtime flag you can flip per request, or per tenant, via a feature-flag service. Resist it at first.
A startup-time switch, one app setting, read once, is much easier to reason about. You deploy a slot pointing at SQL, watch it, and swap back if it misbehaves. Every request in that slot behaves the same. When something is wrong, you know which store it was talking to.
A per-request flag is the right tool later, once you need a gradual rollout: 5% of tenants on SQL, then 25%. But that is a different problem. It needs dual-read comparison, a plan for writes that landed in the wrong store, and a story for what happens to a tenant that flips back. Skip all of that until you actually need a percentage.
Dual writes are the trap
Somebody will suggest writing to both stores during the transition so you can compare. I'd say no unless you have no other choice. There is no transaction across Mongo and SQL. The first time one write succeeds and the other times out, you have a record that exists in one place and not the other, and the reconciliation job you promised to write "later" becomes the main project.
If you need confidence before cutting over, a cheaper option is shadow reads. Serve from the old store, quietly query the new one in the background, log the differences, and never let the shadow result reach the user. It costs you a second query, not a consistency problem.
What I'd actually do on Monday
- Write the contract as a list of business operations. Ten lines. Get both teams to agree on it in a single meeting, because this is the one conversation that can't be skipped.
- Ship the contract and the registration in the common package. One flag, read in one place, defaulting to Mongo.
- Let the SQL team build their implementation behind it without touching Mongo code.
- Run a contract test suite against both implementations. The same tests, parameterised by store. This is what stops the two from drifting.
- Flip the flag per deployment slot. Delete the Mongo implementation when the slot has been on SQL for a full business cycle.
Step four is the one teams skip, and it is the one that makes the toggle trustworthy. A toggle without shared tests is just a hope that two code paths behave alike.
The shared package should be boring. All it should know is the shape of the question, not which database answers it.
Top comments (0)