Experience report
From Monolith to Contract
How OpenAPI became the structural backbone of an AI workflow.
A year of gradual evolution — and a 30-line script that crystallized everything else.
01 — The app that did everything
Mid-2025. I launch Ezkey, an open source MFA project, as a solo developer. Full greenfield. A single Spring Boot application handling everything — admin, authentication, integration. No boundary because there’s only one: a single module, a single deployable, everything in one place.
That’s not a design mistake. In an exploration phase, you don’t yet know what the system needs to become. Better to keep things together and let structure emerge naturally before splitting arbitrarily.
What I knew from day one is that I think in APIs. When I imagine a feature, my first instinct is to define the endpoint, the JSON contract, the HTTP status codes. The user interface, the mobile experience — they will come, of course. But my starting point is always the same: what does the backend contract look like?
02 — The first split: following the trust model
The first reason to split is not architectural — it’s security. The admin will never be publicly exposed. It operates behind a VPN, with entirely different access controls. That’s a clear boundary.
The Auth API follows a different logic: a backend-for-frontend designed to be consumed by the mobile app. Its exposure surface, authentication model, and constraints are completely different from the admin. The Integration API exposes a contract to third-party integrators: another audience, another lifecycle.
Two backends become three, then four with the Crypto API in development mode. And on each backend, consumers multiply: admin UI, mobile app, demo device, Python CLI, shared SDK. Later, experimental projects — ezkey-dart for cross-platform cryptography, ezkey-pam for system authentication. Not for tomorrow, but already there, and they will each need a specification to get started.
The split doesn’t follow an ideal architecture diagram. It follows the trust model of the system. That distinction matters.
03 — Javadoc, OpenAPI: the non-negotiable discipline
From the very first line of code, two non-negotiables: impeccable Javadoc everywhere, without exception. REST controllers fully documented for OpenAPI.
This discipline was not a luxury reserved for a “when the project is stable” stage. It was a founding constraint, accepted from the start. SpringDoc generates the OpenAPI specification dynamically from annotations — a living contract, always in sync with the code, accessible at a stable URL on each running backend.
When backends multiply, this discipline becomes concrete value. Each backend becomes self-describing. A coding agent, a new contributor, a code generator — all can understand a backend’s surface without reading a single line of implementation.
But a contract only has value if it is accessible, fresh, and ready to be consumed. Having it generated dynamically on a live backend is a start. Distributing it to consumers is a different step.
04 — The update-specs script: extract, format, dispatch
The realization comes naturally: if every backend exposes its OpenAPI live, why not extract and dispatch it automatically?
That’s the role of the update-specs script. Its responsibility: connect to each active backend, download the exposed specification, validate it as JSON, format it, then copy it into each consumer sub-project. The mapping is explicit in the script:
- admin-api →
ezkey-admin-ui,ezkey-demo-app-acme,ezkey-sdk - auth-api →
ezkey-mobile,ezkey-demo-device,ezkey-sdk,ezkey-cli
The specification is versioned in the consumer’s sub-folder. It’s a repository artifact, committed, tracked, visible in every diff. When a consumer needs to regenerate its client code — Orval for the admin UI and the mobile app, a Maven plugin for the demo device — it works from this local, versioned file.
A useful detail along the way: Spring exposes its OpenAPI as a single-line JSON by default. The script applies jq . to properly indent it. It’s not revolutionary, but a grep on a multi-line file lets a coding agent target a specific endpoint in a few lines — rather than receiving the entire file.
05 — Slow AI: I am the gatekeeper
One might ask: why not let the agent invoke the script itself? Technically, that’s feasible. An agent can run shell commands. Nothing prevents it.
I don’t do it. Deliberately. I call this approach slow AI.
I’m not looking for maximum agent autonomy. I’m looking for quality human-AI interaction. The dispatch moment is a gating moment: it’s when I, the human, declare that the API is solid enough to become the contract for a consumer.
Before invoking update-specs, I’ve completed the implementation, validated with unit tests, passed functional tests, and walked through the Postman collection against the live backend. I’ve read the generated specification. I’ve verified that the contract actually reflects the original intent.
That reading moment — “does this look like what I had in mind?” — is not automated. It’s a human judgment call, and I intend to keep it that way.
06 — Two sessions, two contexts
The immediate consequence of this workflow: the work naturally splits into two types of sessions.
The backend session starts with a feature plan, covers design, implementation, unit tests, functional tests, Postman validation. When everything is stable and the human judgment is positive, I invoke update-specs. The spec is extracted, formatted, dispatched to the relevant sub-folders. The session closes there. The backend contract is “frozen” — versioned in the repository, placed where it needs to be.
The consumer session starts fresh. For the admin UI: new session, read the previous session’s plan, then read the openapi-spec.json already present in ezkey-admin-ui. The agent works entirely within that sub-project. The backend is not in its context. The contract is defined by the spec — no negotiation, no approximation.
Same logic for the mobile app, the Python CLI, ezkey-dart. Each sub-project has its own local openapi-spec.json, ready to use.
What makes this split effective: the previous session’s plan acts as a condensed context, low in tokens but rich in substance. It contains the decisions, constraints, and design choices — already synthesized, without noise. The agent arrives in the new session with the full picture without having to traverse the entire codebase.
07 — API first, genuinely
I never start a feature with a mockup. I start with a vision that takes shape through APIs and a database schema. The user interface, the mobile experience — they come, and they matter. But my drive-out from the start is always the backend contract. That’s what “developer-first, backend-first” means in practice at Ezkey — not just a positioning statement, a wired reflex.
The spec dispatch workflow materializes this philosophy structurally. The spec is the founding act of each vertical slice. Until it is validated and dispatched, nothing else starts: no admin UI work, no mobile development, no SDK generation.
OpenAPI, spec-first, bounded contexts — recommended in books for years. Rarely followed rigorously in practice, often deferred to “when the project is more stable.” Here, these practices stop being optional by construction: there is no consumer session without a validated spec. The contract comes first.
The experimental projects confirm the same logic: ezkey-dart will start from a generated specification to build its SDK. ezkey-pam likewise. Same philosophy, same starting point. This is no longer a good intention. It’s the constraint that structures everything else.
08 — What this actually taught me
“Context engineering” — a fine expression. You often see it presented with compression plugins, automatic synthesis pipelines, entire frameworks dedicated to context optimization. All of that can make sense in certain team contexts.
My pragmatic translation, for a solo developer who values simplicity: context is an architectural choice, not a configuration product. Fresh sessions, well-bounded subjects, versioned contracts — those are the levers that actually made the difference.
Slow AI is not a limitation. It’s a positioning that preserves the quality of decisions. The agent is powerful — but it is most powerful when it knows exactly where it is and where to stop. And it’s me who decides where that boundary sits.
The best context is not the most complete. It’s the one that knows when to stop.
French version: Du monolithe au contrat
See also: From code to intent — one year of AI workflow
See also: The AI coding manifesto — bounded contexts and agent mode
See also: Generative AI and No-Code — the context engineering constant