How do you deprecate a shared business-action signature that hundreds of live cases call, without a flag day?
answer
- A sequencing device, not a promise
- New beside old, never instead of
- The old form delegates; behaviour cannot fork
- Batches you can review and revert
- Freeze the denominator, then delete
basics
~20 sAdd the new signature beside the old one, make the old one delegate to it so behaviour cannot fork, move callers in reviewable batches, reject new uses once none are arriving, then delete the old form entirely.
solid answer
~50 sBecause every caller lives in the same repository, deprecation here is a **sequencing device, not a compatibility promise** — you are staging a change you could in principle make in one commit, so that review, revert and blame stay usable. Introduce the new action alongside the old; reimplement the old one as a thin call into the new so the two can never behave differently; move callers in batches small enough to review and revert independently; once no new calls arrive, make the build reject additions while permitting the sites already listed; then delete the old signature and its warning together. Two rules make it safe: never let the old and new paths carry separate implementations, and never leave the deprecated form standing after the last caller moves — an unused old signature is a decision every future reader has to make again.
code
pseudocode · 12 lines# step 1: the new signature carries the real implementation
action open_account(actor, plan, region):
...real work here...
# step 2: the old signature becomes a thin door onto the same room
action open_account(actor, plan): # deprecated
warn_once("two-argument open_account moves to the three-argument form")
return open_account(actor, plan, "home") # provably what it always used
# step 3: callers move in batches, old copy deleted in the same change
# step 4: build rejects NEW two-argument calls, permits the listed ones
# step 5: delete the two-argument form and its warning togethergo deeper
Know what deprecating means in practice: the old way still works, it is marked as going away, and callers are expected to move. Be able to say why deleting it outright breaks everyone who pulls next.
Walk the sequence and justify each step — new alongside old, old delegating to new, callers in batches, then deletion — and explain that one shared implementation is precisely what stops the two paths diverging.
Show how you make the burndown mechanical: a build rule that rejects new uses while permitting the listed ones, a visible count, and a default supplied only where you can prove it matches what callers already got.
Argue when the staged route is not worth it. Where you can see and change every caller, one mechanical change is sometimes cheaper than a migration window, and a deprecated path that outlives its callers is a liability the whole team pays to read.
## Why this is not the same as evolving a published contract A business action in a harness — `open_account`, `place_order`, `sign_in_as` — is called only from cases in the same repository. You can see every caller. You could, in principle, change all of them in one commit. That single property changes what deprecation *is* here: it is not a compatibility promise made to strangers, it is a **sequencing device** you use on yourself so that review, revert and blame stay usable while a few hundred call sites move. Understanding that keeps the discipline honest in both directions. It rules out the flag day, where the signature changes in place and everyone who pulls next inherits a broken suite they did not cause. It also rules out the opposite failure, where the old form is marked as going away and then lives forever, because nobody is coming to force the issue. ## The sequence 1. **Introduce the new action beside the old one.** The new form carries the real implementation. Nothing else changes yet, so this step cannot break a caller. 2. **Reimplement the old form as a thin call into the new one.** This is the step that makes the rest safe. With one implementation behind both doors, any behaviour fix landed during the migration reaches migrated and unmigrated callers alike. Two implementations, however briefly, means a case can pass on one path and fail on the other for reasons that have nothing to do with the product. 3. **Move callers in reviewable batches.** Batch size is set by what a reviewer can actually read and what you can revert in one piece — not by how many sites a script can touch at once. 4. **Freeze the denominator.** Once no *new* calls are arriving on the old form, make the build reject additions while permitting the sites already on the list. The remaining count can then only fall, and no single team is blocked on one large change. 5. **Delete the old form and its warning together**, in one change, the moment the last caller moves. | Approach | Behaviour risk | Review risk | When it fits | | --- | --- | --- | --- | | Change the signature in place | Low | High — one enormous diff, all-or-nothing revert | A handful of callers you can read at once | | Old form delegates to new | Low — one implementation | Low — small batches, independent reverts | Hundreds of callers, several teams | | Old and new kept independent | High — the two paths drift | Low per change | Never inside one repository | | Old form kept indefinitely | Low now, rising later | Low | Never; it is a decision deferred, not avoided | ## The trap: a parameter the old callers never supplied The common shape of this change is not a rename; it is a new piece of information the action now needs — a region, a plan, an identity, a mode. The delegating form has to supply something, and the tempting move is a default. That default is where signature migrations turn into behaviour migrations. - If you can **prove** the default is exactly what every existing caller was already getting — because the old implementation hardcoded it — then a default is a faithful description of the status quo. Say so in the shim, next to the value, so the next reader does not have to rediscover it. - If you cannot prove it, do not guess on behalf of a few hundred cases. Make the value a per-batch decision so a human picks it with the case in front of them. A silently defaulted parameter turns every unmigrated case into a case that exercises something nobody chose, and because the suite still passes, nothing tells you. ## Measuring it, so it actually finishes A deprecation that is announced but not measured is a deprecation that does not happen. Three cheap instruments cover it. First, a **visible count** of remaining callers, published with the same cadence as any other burndown. Second, the **build rule** from step 4, which converts the count from a moving target into a strictly decreasing one. Third, a **named end**: the change that deletes the old form is a planned piece of work with an owner, not something that happens when the count reaches zero by accident. The last discipline is the hardest to keep and the cheapest to state: **do not leave the deprecated action behind once it is unused.** An unused-but-present old form costs nothing to run and a great deal to read. Every future engineer who meets both signatures has to work out which one is current, whether the old one has a reason to exist, and whether the thing they are about to write should use it. That is the same decision, made again, by everyone, forever — which is a far larger total cost than the one change that would have removed it.
- The replacement action needs a value the old callers never supplied. How do you avoid guessing it for hundreds of cases?Do not let the delegating form invent one silently. Either pick a value you can prove every existing caller already got, because the old implementation hardcoded it, and say so beside the value; or make it a per-batch decision so a human chooses with the case in front of them. A silently defaulted parameter turns a signature change into a behaviour change that no case asserts and no reviewer sees.
- When does the deprecated signature stop being tolerated and start failing the build?Once no new calls are arriving and the count is falling. Move from a warning to a rule that rejects additions while permitting the sites already listed. That freezes the denominator so the burndown can only go down, and it does it without blocking any team on one large change.
- How do you keep the old and new signatures from drifting apart while both exist?Give them one implementation. The deprecated form must be a thin adapter that calls the new one and does nothing else: no branch, no second path, no fix applied to only one side. If behaviour must change mid-migration, it changes in the new implementation and every remaining old caller inherits it for free.
saying these in an interview costs you the question
- Changes the signature in place and fixes the fallout afterwards
- Leaves the old and new forms with separate implementations
- Keeps the deprecated action forever in case someone needs it
- Silently defaults the new parameter for every existing caller
- Moves all callers in one unreviewable change
- Announces the deprecation and never measures the remaining callers