Wrap, replace, or modernize? Deciding what to do with a legacy interface
A decision framework for legacy interfaces: when to put a facade in front, when to rebuild, and when to change the interface itself.
A legacy interface is any interface that other things depend on and that your team would not design the same way today. A SOAP endpoint, a stored procedure that three applications call, a CSV drop that a partner polls at midnight, an internal REST API with a dozen undocumented fields. The question is never whether it is ugly. The question is what to do about it, and in what order.
There are three honest options. Each is right in some situations and expensive in others.
The three options
Wrap. Leave the interface and its implementation alone. Put a new interface in front of it that translates to and from the old one. Callers migrate to the new interface at their own pace.
Replace. Build a new implementation behind the existing interface. Callers notice nothing, if you have done it right. The interface contract is preserved exactly; the code behind it is new.
Modernize. Change the interface itself. New shape, new semantics, new error model. Callers must change. The old interface is retired on a schedule.
Teams often mean “modernize” when they say “replace”, and vice versa. Agree on the vocabulary before the meeting.
What you need to know before choosing
Each option depends on a different fact about the system. If you do not have the fact, the choice is a guess.
- How many callers, and who owns them? Observed from traffic, not from a list someone wrote two years ago. Wrapping and modernizing both require callers to move. Replacing does not.
- What is the actual contract? Not the documented one. The fields callers send, the fields they read, the error codes they handle, the timing they tolerate. Replacement is only safe when this is verified, because the new implementation must honor all of it.
- Which behaviors are accidental? A 255-character truncation, an undocumented retry, a timezone quirk. Some callers depend on these. Replacing must preserve them; modernizing is the chance to drop them, deliberately.
- How testable is the current behavior? If you can write conformance tests against the existing interface, replacement becomes tractable. If you cannot, wrapping limits the blast radius while you learn.
- What is the cost of being wrong? Order totals and payments are not the same as an internal reporting feed.
A decision rule
Use this as a starting point, then argue with it.
- If you cannot yet state the current contract with verified findings, wrap first. A facade gives you a place to observe traffic, enforce a schema, and learn what callers really do. Do not replace what you cannot describe.
- If the contract is verified and the implementation is the problem (unmaintainable, unsupported runtime, cost), replace. Keep the interface byte-for-byte compatible, run the conformance suite against both implementations, and switch traffic gradually.
- If the interface itself is the problem (wrong abstraction, leaks internals, blocks a product need) and you control or can influence the callers, modernize. Publish the new interface, provide a translation layer for stragglers, and set a retirement date with evidence that every caller has moved.
- If the interface is the problem and you do not control the callers, wrap and wait. The facade becomes the modern interface for new callers; the old one shrinks until retirement is cheap.
Most real programs do more than one of these in sequence. Wrap to learn, replace to stabilize, modernize when the callers are ready.
Wrapping without creating a second legacy
A facade becomes a liability when it accumulates logic. Rules that keep it honest:
- The facade translates. It does not decide. Business rules stay in one place.
- Every request through the facade is logged with enough detail to reconstruct the contract later. This is the whole point.
- The facade enforces the schema you intend to support, and reports, rather than silently fixes, requests that violate it.
- It has a retirement condition written down on day one.
Replacing without breaking callers
Replacement lives or dies on the conformance suite. The suite should cover:
- Every documented behavior.
- Every discovered behavior that any caller is observed to rely on, including the accidental ones.
- Error responses and status codes, not just happy paths.
- Timing-sensitive behavior: timeouts, retry windows, idempotency periods.
Run the suite against the old implementation first. If it does not pass there, the suite is wrong. Then run it against the new one. Then run both implementations in parallel on real traffic and compare, before anything depends on the new one alone.
Behaviors that remain inferred rather than verified are the ones that will break. Decide for each whether to test it, preserve it defensively, or accept the risk in writing.
Modernizing without losing the knowledge
The risk in modernizing is not technical. It is that the reasons behind the old interface’s oddities are lost, and the new design repeats a mistake that the old one had already fixed. Before designing the new interface, write down each quirk of the old one with the best available explanation and its label. “Truncates notes at 255. Inferred: legacy column width. Three callers send longer notes today.” Each such line is a design decision the new interface has to make deliberately.
What not to do
- Do not replace and modernize at the same time. Two variables, one experiment.
- Do not trust a caller inventory that is not backed by recent traffic.
- Do not set a retirement date without a way to measure who still calls the old interface.
- Do not treat “we have unit tests” as “we have a conformance suite”. Unit tests test the implementation. Conformance tests test the contract.
Where Foundry fits
Foundry establishes the facts the decision depends on: the contract callers actually use, reconstructed as an OpenAPI specification; the behaviors verified by conformance tests; the dependency map of who calls what; and a change-impact assessment that says which behaviors must survive. See the Advance use case.