Skip to main content
Back to blog
By Steve Ackley·

The Swiss Cheese Method: deterministic templates plus LLM logic

By Steve Ackley · April 18, 2026 · 8 min read

Corrected October 4, 2026: earlier versions listed Supabase auth pages, Stripe scaffolding, shadcn/ui, health checks and GitHub Actions CI as deterministic scaffolding, and business logic as LLM output; generated repos include none of those, and the LLM writes per-entity CRUD code.

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 project structure, DI and config, logging, OpenAPI, the Docker setup. The LLM only gets to touch specific, bounded, well-typed holes.

What is deterministic

If you generate an app with StackAlchemist, much of the code that ends up in your zip is deterministic. Every app ships with the same:

  • Next.js 16 App Router project structure
  • .NET 10 minimal API project structure with the same namespace convention
  • Program.cs with DI, Serilog logging, OpenAPI, CORS and configuration
  • The database connection factory the Dapper repositories use
  • The SQL migration file the per-entity tables are spliced into (UUID keys, row-level security enabled)
  • Tailwind, already configured
  • Multi-stage Dockerfile + docker-compose for local dev, and .env.example
  • The Supabase client (@supabase/supabase-js) preinstalled with its env slots, and nothing calling it: the auth flows are yours to write

What is not in the scaffold: auth pages, Stripe, CI workflows. Those are yours to wire.

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 Program.cs, the DI container and the database connection 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? In Simple mode the LLM proposes them from your description (you can edit them before generating), then drafts the C# records and the matching TypeScript types.
  • CRUD code per entity. The Dapper repository, the API endpoints, the table's migration fragment, the typed API client and the pages. Advanced Mode can also declare custom endpoints in the schema.
  • Domain-specific UI copy. Labels and text on the generated pages. These need to match the domain.

Note what is not on this list: routing, file structure, build config. If we let the LLM generate those, we would be back in full-LLM-hallucination land. Also not on it: business rules, workflows, background jobs, integrations. The LLM writes CRUD-shaped domain code; the rules your product runs on are yours to write.

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, code elsewhere 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:

  1. Typed contracts between template and LLM output. Every hole in the template has a declared place: specific file paths and named zones. The LLM's output is checked against them before insertion. Output aimed at a path or zone the template did not declare is rejected.
  2. Schema validation before any code is written. The schema the LLM extracts from your description is checked before generation starts: every relationship has to point at an entity that exists. A schema that references a missing entity never reaches the code step.
  3. The compile gate. Everything then runs through dotnet build, the TypeScript typecheck and next build, with the compiler errors fed back to the model for up to three repair attempts. 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: path and zone validation is microseconds, schema validation is milliseconds, full compile is minutes. Whether a route's types are actually in scope is the compiler's call. There is no cheaper check for it that I trust. 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 most of a codebase. The wiring between a route, its handler and the DI container 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, CRUD code, and UI copy.
  • Your domain logic is the part that actually needs editing. The Docker config and the project plumbing — you can leave those alone. The business rules you add on top of the generated CRUD code are where you will iterate, along with the auth and payments you wire in. 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 handles what does not need creativity: routing, DI, config, logging, Docker, the migration file.
  • LLM-generated holes handle what does: domain models, per-entity CRUD code, 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 per-entity files under Models/, Repositories/ and Controllers/ are the LLM's; the plumbing around them is template. I'm transparent about this because it turns out to be the part developers most want to understand.

— Steve