Let It Stabilise First
Don't commit to a structural contract before you know what you're agreeing to. On deferring irreversible decisions until real usage has revealed the domain.
7 June 2026 5 min read
Midway through building an API, I made a decision I initially read as a concession to time pressure. Several of the data shapes weren't settled yet. Stakeholders were reviewing the system regularly and finding things to adjust. I didn't want to be changing typed contracts every sprint, so I left certain endpoints returning dynamic types, with no enforced schema.
This felt like the wrong thing to do. The standard discipline says type your interfaces early: it catches contract violations at compile time, it documents your expectations for callers, it makes refactoring safe. Leaving things untyped is the kind of shortcut that creates technical debt.
About four months later, the API had a properly typed surface. The migration was clean because it happened after the shapes had settled under real operator use.
Looking back, I don't think the untyped period was a shortcut. It was the correct sequencing.
The Cost of Premature Contracts
A typed contract is a commitment. Once you define the shape of a request or response, you have stated what the API expects. Callers build against that statement. Changing it later means changing them too.
This is exactly why typed interfaces are valuable in stable, well-understood domains. The type system encodes domain knowledge, and it propagates corrections when that knowledge changes. The types are trustworthy because the domain is understood well enough to write them correctly.
But early in a system built against real operational workflows, you often don't have that. You have a first approximation. The stakeholders think they know what they need; the developer has a model of those needs; and both will be revised as soon as real usage begins. Requirements gathering and upfront design can get you close. What they cannot do is expose the edge cases, the workflow details, and the inconsistencies that only become visible when people use the system for real work.
If you commit to a typed contract based on your first approximation, you're locking in a design you know will be revised. Every revision means breaking the contract, notifying callers, and updating both ends, and the more structural the original contract the more each revision costs. What you pay for a premature contract isn't the original typing. It's every update after it, multiplied by the number of things that depended on the shape you guessed.
What Stabilisation Looks Like
The useful question is not "should I type this?" but "do I know enough about this domain to commit to a shape?"
For several months in this project, the answer was no. Operators were discovering their own workflows as they used the system. Features that seemed well-defined in sprint planning became ambiguous when someone tried to use them for a real task. Response shapes that made sense at design time turned out to omit data that was needed in practice, or to structure data in a way that didn't match how the frontend needed to consume it.
The dynamic types weren't a retreat from discipline. They were an acknowledgment that the domain knowledge was still arriving.
When things did settle, the migration was straightforward. Each endpoint had been used enough times that I could look at the actual consumption patterns, understand what the system expected, and define a type that matched reality rather than a projection of what reality might be. The type I ended up with was different from what I would have written four months earlier: not dramatically, but in ways that would have made the earlier version wrong.
One outcome that surprised me: several query endpoints that handled freeform operator-defined metadata stayed dynamic by design even after the migration. The metadata was open-ended by nature. A typed shape would have been either too rigid, excluding legitimate values, or too permissive, accepting anything, which gives you the same behaviour with more syntax. Keeping those endpoints dynamic was itself a product of the stabilisation process: it showed that the right answer for that part of the API was never "type it later", it was "this doesn't have a stable shape, by design."
The Lean Framing
Lean software development has a principle about this: decide as late as responsibly possible. The point is to delay the irreversible decisions until you have the information needed to make them well, which is not the same as delaying everything.
A typed API contract is, in principle, reversible. You can migrate it. But migration has a cost, and that cost scales with how much has been built against the original contract. The later you revise it, the more expensive the revision. So "as late as responsibly possible" means: delay until the domain is understood, but not so long that the cost of revision has become prohibitive.
The right window is during active development, after a few real iterations with users but before the API surface has become a dependency for more than you can easily update.
The Broader Application
The same logic applies beyond API contracts. It applies to database schemas, to the boundaries between services, to the naming conventions you encode in test fixtures, to any structural decision that will be expensive to change once other things depend on it.
The discipline is to identify the decisions that will be expensive to revise and make sure you have real usage data before you make them. If a decision is easy to revise, make it now and update it when you learn more. If it's hard to revise, wait until the domain has shown you enough to make it correctly, the way those query endpoints eventually showed me they were never going to hold a shape at all.