The Swiss Cheese Method: deterministic templates plus LLM logic
By Steve Ackley · April 18, 2026 · 8 min read
In my last post I wrote about why a compile guarantee is the minimum bar for a real AI code generator. Several people asked the obvious follow-up: "OK, how do you actually do it?"
This is the technical answer. We call it the Swiss Cheese Method because the analogy is the one that made it click for me when I was first designing the engine: the cheese is deterministic; the holes are where the LLM gets to play.
The two failure modes
Every AI code generator in the market falls somewhere on a spectrum between two failure modes.
On one end is full LLM generation. Give the model a prompt, let it emit every line of code in the entire repo. This is what the first-generation tools did. The failure mode is obvious: the model hallucinates. It imports libraries that do not exist. It references database columns that were never created. It forgets what it wrote in file A when it generates file B. The codebase diverges from itself on every generation.
On the other end is pure template engines. You pick from a menu of pre-built SaaS templates, fill in a few variables, and get a deterministic output. The failure mode here is that templates are rigid. You get what the template author imagined, nothing more. If your domain does not fit the template, you are cut off at the knees.
The Swiss Cheese Method is a deliberate middle. Most of the repo is deterministic template — the scaffolding, the auth wiring, the database migrations, the build pipeline, the Docker setup, the CI/CD. The LLM only gets to touch specific, bounded, well-typed holes.
What is deterministic
If you generate an app with StackAlchemist, about 85% of the code that ends up in your zip is deterministic. Every app ships with the same:
- Next.js 15 App Router project structure
- .NET 10 Web API project structure with the same namespace convention
- Supabase auth wiring (login/register/password reset routes are byte-for-byte identical across generations)
- PostgreSQL migration tooling
- Dockerfile + docker-compose for local dev
- GitHub Actions CI that runs lint, typecheck, unit tests, and a build verification step
- shadcn/ui component library, already wired
- Error boundaries, logging setup, health check endpoints
- A pricing page skeleton (yes, we give you Stripe scaffolding by default)
This code is generated by Handlebars templates, not by the LLM. It is identical across every customer's generation because there is no reason for it not to be. The wiring between app/login/page.tsx and Supabase's signInWithPassword is not a creative problem. Solving it the same way every time is not just fine — it is the right call.
What is LLM-generated
The holes — the parts that actually differ between a ceramics marketplace and a fintech SaaS — are where the LLM earns its keep. Specifically:
- Domain models. What entities live in your system? What fields do they have? What are the relationships? The LLM drafts the TypeScript interfaces and the matching EF Core entities. A deterministic pass then ensures they stay in sync.
- Business logic functions. The LLM writes the actual service-layer methods. "When a customer submits an order, do X, Y, Z."
- Domain-specific UI copy. Hero headings, form labels, empty-state messages. These need to match the domain.
- Example data / seed records. The LLM generates realistic seed data for your domain — so when you run the app locally you see something that looks like a real ceramics marketplace, not "Product 1, Product 2."
- README.md. Describes your specific generated app, not our template.
Note what is not on this list: routing, auth, file structure, build config. If we let the LLM generate those, we would be back in full-LLM-hallucination land.
The interface between cheese and hole is where it all breaks
Here is the part most teams get wrong. You can have a clean deterministic scaffold. You can have a clean LLM-generated domain model. And it all still blows up, because the interface between them is not typed, not verified, not enforced.
Example: the LLM decides your ceramics marketplace has an entity called CeramicListing with a field artisanId. Meanwhile, the deterministic auth template assumes user records are queried by userId. Nothing stops those two from diverging. The code compiles in isolation. It fails the first time anyone tries to query "listings for the current user."
Our engine enforces the interface in three ways:
- Typed contracts between template and LLM output. Every hole in the template has a declared schema. The LLM's output is validated against that schema before insertion. If the LLM emits an entity without the required foreign-key field to the user table, the insertion fails and the LLM is prompted again with a specific error.
- Post-generation integrity checks. After the LLM-generated entities are written, we run a static pass that walks the dependency graph: does every entity reference to a user compile? Does every API route that claims to return
CeramicListingactually have that type in scope? - The compile gate. Everything then runs through
dotnet build,pnpm build, and a smoke test. This is the one from the previous post. It is the backstop, not the primary check — but if the first two passes let a bug through, this catches it.
Three checks, in order of cost: type validation is microseconds, integrity checks are milliseconds, full compile is minutes. We fail fast at the cheap levels and only pay the compile cost when we believe we are going to pass it.
Why I chose determinism over flexibility
The implicit argument of every "prompt to app" tool is: more LLM = more magic. The pitch is that with the next model generation, the hallucinations will go away and the tool will become a god-mode code generator.
I don't buy it. Not because the models won't improve — they obviously will — but because even a perfect LLM is the wrong tool for 85% of a codebase. The routing between app/dashboard/page.tsx and the auth middleware is solved. It has been solved since 2014. There is no creative value in having an LLM re-derive it every time. Every token the LLM spends re-writing boilerplate is a token it is not spending on your actual business logic.
Determinism is not a weakness we are hiding. It is a feature we charge for. When you generate with StackAlchemist, the scaffolding you get is the scaffolding I would write myself on a greenfield project — because it is, almost literally, the scaffolding I wrote on a greenfield project, parameterized and templated. That is the accumulated taste of a senior engineer, baked in. The LLM is the junior engineer writing the domain code under that senior engineer's supervision.
What this means for your output
Practically, the Swiss Cheese architecture means:
- Your generated repo looks like a human wrote it. Because most of it was written by a human — me — and then parameterized. The LLM-generated parts are small, bounded, and reviewed against a schema.
- Two customers in the same vertical get different apps. The LLM makes the difference. A ceramics marketplace and a vintage-watch marketplace share scaffolding but diverge sharply in domain models, business logic, and UI copy.
- Your domain logic is the part that actually needs editing. The auth wiring, the Docker config, the CI — you can leave those alone. The service-layer methods are where you will iterate. This is exactly where a senior engineer would expect to iterate.
Key takeaways
- Full LLM generation hallucinates. Pure templates are rigid. The Swiss Cheese Method is the deliberate middle.
- Deterministic scaffolding (85% of the code) handles what does not need creativity: auth, routing, CI, Docker, migrations.
- LLM-generated holes handle what does: domain models, business logic, UI copy.
- The interface between the two is enforced by typed contracts and integrity checks.
- Most of the value of a senior engineer's taste is in the scaffolding, not the domain logic. Bake the scaffolding; let the LLM handle the novelty.
If you want to see what a Swiss Cheese output looks like in your hands, generate one. The README will tell you which files were template-generated and which were LLM-generated — we are transparent about this because it turns out to be the part developers most want to understand.
— Steve