The shape of it
Why this needed to be more than one service
A build has four phases with genuinely different resource profiles: interpret the prompt, generate code, store artifacts, run a preview. Generation is slow and I/O-bound against a model API. Preview provisioning is a Kubernetes operation that can take a minute and then hold resources indefinitely. Putting these in one process means one slow build blocks everything and one crash loses all four phases.
So the platform is split, with Spring Cloud supplying the boring infrastructure: Eureka for discovery, Config for centralised configuration, and Gateway as the single entry point handling routing and auth for the whole system.
Where the AI part sits
Code generation runs through Spring AI against OpenAI, with two things that make output usable rather than plausible: tool-calling, so the model requests file operations through functions I control instead of emitting a blob I have to parse; and RAG-style context injection, so the prompt carries the project's existing files and conventions and the model edits a real codebase rather than inventing one.
The gateway is the only public surface. Everything behind it discovers its peers through Eureka and talks over Kafka rather than direct calls.
Hard problem 01
A build that half-succeeds
Four services, four chances to fail. Code generates, artifacts upload, and then the preview pod fails to schedule because the cluster is out of capacity. Now there's a project record claiming to be live, storage holding an orphaned bundle, and a user staring at a URL that never resolves.
There's no distributed transaction available here — Kafka, MinIO and the Kubernetes API cannot participate in a two-phase commit, and I wouldn't want them to. So the build is modelled as a Saga: a sequence of local transactions, each publishing an event that triggers the next, and each with a defined compensating action that undoes it.
When provisioning fails, the saga runs backwards. Artifacts are deleted, the project is marked failed with a reason, and the user gets an error instead of a broken link. The system ends in a state someone chose, rather than wherever the crash left it.
Each step knows how to undo itself. Failure at any point walks the chain back to a clean, explained state.
Hard problem 02
Kafka will deliver that twice
At-least-once delivery is the honest default, which means every consumer will eventually see a message it has already processed — after a rebalance, a retry, or a slow commit. If the provisioning consumer isn't careful, one duplicate becomes two pods for one project, and the cluster pays for it.
So consumers are idempotent by construction. Each carries the build's identifier as a deduplication key and checks state before acting: if a pod already exists for this build, the handler acknowledges and returns rather than provisioning again. The effect of processing a message twice is identical to processing it once.
Preview provisioning
Each successful build gets a pod allocated dynamically, with the generated bundle synced from MinIO and Redis caching the metadata a preview needs so a page load doesn't fan out into three service calls.
What I'd do next
- Give preview pods a TTL and a reaper, since the current design assumes someone eventually cleans up.
- Add distributed tracing across the saga — right now diagnosing a stuck build means reading four services' logs side by side.
- Move dedup keys out of service memory into a shared store, so a restart mid-saga can't reprocess.