How we think

Template-Based AI Engineering

The model doesn't design your architecture - it instantiates one you've already verified. Why a curated template library outperforms model upgrades, fine-tunes, and prompt heroics.

Run an experiment. Take the same model, the same task - “add a service that manages orders: persistence, API, tests” - and run it twice. Once in an empty repository. Once in a repository seeded with a working service template: layered structure, configuration conventions, auth wiring, a documented transaction manager, a passing test suite.

The first run produces something plausible. It compiles. It is also an architecture nobody chose: a session opened inline in a route handler because a tutorial did it that way, configuration read three different ways, error handling that exists where the training data happened to have it. Every decision was made by statistical gravity.

The second run produces a service that looks like it was written by whoever wrote the template. Same layering. Same naming. Write paths inside the transaction decorator, read paths on the session decorator, config loaded the one house way. The model made almost no architectural decisions - and that is precisely why the output is good.

That is the entire thesis, so it deserves its own line: the model doesn’t design the architecture - it instantiates one you’ve already verified. Generation stops being design and becomes instantiation. Everything else in this piece is the mechanics of making that happen on purpose.

Local patterns are the lever

An LLM writing code draws from two sources. The first is its training distribution: the public ocean of tutorials, Stack Overflow answers, abandoned side projects, and documentation examples - code optimized to demonstrate one concept in isolation, almost never to run in production next to everything else. The second is whatever is in front of it right now: the repository, the conventions, the documents in its context.

The model’s strongest behavior is mimicry of the second source. Show it three repositories’ worth of a pattern and it will produce a fourth instance with startling fidelity. Its weakest behavior is unsupervised synthesis from the first source - that is where the generic, subtly incoherent architecture comes from. Most teams fight the weakness with longer prompts. The higher-leverage move is to feed the strength: engineer what the model sees.

We wrote in The Three Disciplines that outcome-led work - the discipline where the AI owns the implementation loop and the human reviews at the PR boundary - has a hard precondition: the codebase must already have established local patterns the model can mimic. On a mature codebase that precondition is either met or it isn’t. On a greenfield build it is never met. Day one of a new repository has no local patterns at all, which should force every task down into line-by-line review and keep it there for weeks.

A template library breaks that constraint. It is manufactured local patterns - the precondition for high-autonomy work, imported on day one instead of accreted over months. That is the strategic content of this piece: templates are not a convenience for starting projects faster. They are what makes the faster disciplines available.

A template is not boilerplate

The distinction matters. Boilerplate saves typing - a folder structure, a build file, a hello-world endpoint. A template carries decisions. The difference shows up in what happens to the next thousand lines written on top of it.

A real template is a complete, internally consistent exemplar system. The application shape agrees with the configuration convention, which agrees with the auth wiring, which agrees with the logging format, which agrees with the container build, which agrees with the infrastructure module that deploys it. Every layer answers the question “how do we do this here?” the same way. It is a taxonomy of code expressed as running code: what correct looks like, what complete looks like, and how the pieces compose.

That internal consistency is what the model actually consumes. A single pattern in isolation teaches the model one trick. A system where the persistence pattern, the error handling, the DTO boundaries, and the test structure all reinforce each other teaches the model the grammar - and the next service it writes is a sentence in that grammar rather than a collage of dialects.

A claim you can test against your own stack: if your “starter” repo is a folder structure with TODOs in it, the model will fill the TODOs from the training ocean, and you will get the empty-repository result with extra steps. If your starter repo runs, passes its own tests, and documents its own contracts, the model extends it. The gap between those two outcomes is the gap between scaffolding and architecture.

A worked example: the transaction manager

Here is the kind of asset that does the heavy lifting, from our Python service template. Persistence is the place where generated code fails quietly: sessions opened per query, transactions that span nothing, commits sprinkled until the tests pass. It fails quietly because the happy path works - the damage only shows under concurrency and partial failure, which is to say, in production.

The template answers persistence once, as a small piece of infrastructure. A thread-local session with reference counting, so nested calls share one session instead of opening their own:

lib_common / database.py
class Database:
    engine = None
    SessionLocal = None
    Base = declarative_base()
    _thread_local = threading.local()

    @staticmethod
    def configure(database_url: str, engine_args: dict = None):
        """Configure the database. Call this once during app startup."""
        Database.engine = create_engine(database_url, **(engine_args or {}))
        Database.SessionLocal = sessionmaker(
            autocommit=False, autoflush=False, bind=Database.engine)

    @staticmethod
    def get_session():
        # One session per thread, reference-counted so nested
        # decorators share it instead of opening their own.
        if not hasattr(Database._thread_local, "session"):
            Database._thread_local.session = Database.SessionLocal()
            Database._thread_local.session_usage_count = 1
        else:
            Database._thread_local.session_usage_count += 1
        return Database._thread_local.session

    @staticmethod
    def close_session():
        # Only the outermost holder actually closes.
        Database._thread_local.session_usage_count -= 1
        if Database._thread_local.session_usage_count == 0:
            Database._thread_local.session.close()
            del Database._thread_local.session

On top of it, two decorators that split the world into a write path and a read path - and documentation that states the contract, not the syntax:

lib_common / decorators
def transactional(func):
    """Write path. The decorated function runs inside a transaction.

    On success: commit. On ANY exception, caught or not: rollback.
    Session cleanup is guaranteed. There is no path through this
    decorator that leaves a transaction open.
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            Database.get_session()
            Database.begin_transaction()
            result = func(*args, **kwargs)
            Database.commit_transaction()
            Database.close_session()
            return result
        except Exception as e:
            Database.rollback_transaction()
            raise e
    return wrapper


def with_session(func):
    """Read path. Acquires a session, never opens a transaction.

    Use on route handlers and query-only operations. Lazy loads
    still work; nothing is committed because nothing changed.
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            Database.get_session()
            return func(*args, **kwargs)
        finally:
            Database.close_session()
    return wrapper

And composition that resolves correctly without anyone thinking about it:

usage - composition under nesting
@router.post("/api/books/create")
@with_session                          # outer: acquires the session
def add_book(book_create_dto: BookCreateDto):
    book = book_service.create_book(book_create_dto)
    return BookDto.from_model(book)


class BookService:

    @transactional                     # inner: reuses the SAME session,
    def create_book(self, dto):        # owns the transaction boundary
        author = author_repo.find_by_name(dto.author_name)
        book = Book(title=dto.title, author=author)
        book_repo.save(book)
        return book

# Nesting resolves through the usage counter:
#   route   get_session()  -> count 1
#   service get_session()  -> count 2  (same session)
#   service commit + close -> count 1  (commit happens, session stays)
#   route   close          -> count 0  (session actually closes)

Now the part that makes this an AI story rather than a code-review story. Ask a model to add a feature in a repository containing this - the code and the docstrings stating the contract - and it wires persistence correctly. Not because it reasoned about transaction semantics, but because the local pattern is unambiguous, the naming says what each path is for, and every existing call site demonstrates the usage. The same model in a bare repository invents tutorial-grade session handling, because tutorials are what the ocean contains. Same model. Same prompt. The variable is what it could see.

One more property worth noticing: our Java service template answers the same question with the same shape - declarative transaction boundaries at the service layer, repositories below, DTOs at the edges. The Python decorators deliberately mirror those semantics. An engineer who learns the grammar in one language reads the next language’s template as a translation, not a new system - and so does the model. Cross-language consistency multiplies the library’s teaching power without adding a single document.

And the pattern choice itself is grounded the same way. Line up how mature ORMs across languages answer session and transaction management, and the house pattern stops looking like a preference:

FrameworkPass session explicitly?Manual commit()?Declarative transactions?
Spring Data JPA (Java)NoNoYes - @Transactional
Hibernate (Java)NoNoYes - @Transactional
Django ORM (Python)NoNoYes - @transaction.atomic
Rails ActiveRecord (Ruby)NoNoYes - transaction do
Entity Framework (C#)NoNoYes - automatic
SQLAlchemy - common community patternYesYesNo
SQLAlchemy - our templateNoNoYes - @transactional

Five ecosystems converged on declarative boundaries independently. The template brings Python in line with the grammar the rest of the stack already speaks - which is the level templates operate at: not “here is a starter repo,” but a position on how persistence should work, checked against how the industry’s mature frameworks answered the same question, expressed as forty lines of infrastructure the model can read.

Greenfield: seed first, then ask

The two halves of engineering work - building new and changing existing - bias the model in opposite directions, and the template plays a different role in each.

On new code the model has nothing to anchor to, so it anchors to the training distribution. Every choice you did not constrain gets made by what was statistically common in public code - which is how you end up with an Express habit in a FastAPI service and three configuration idioms in one repository. The failure isn’t that any single choice is wrong. It’s that nobody made them.

The fix is ordering. Seed the repository with the template before the first feature ask - not as a reference in the prompt, but as the actual starting state of the repo. The first feature then lands inside the house architecture, and here is the compounding part: that feature becomes additional local pattern mass for the second feature. Each generation cycle reinforces the grammar instead of diluting it. Get the first week right and the repository teaches the model itself from then on; get it wrong and you are correcting the same architectural drift in every review until someone schedules the cleanup.

This is also where the disciplines connect in practice. With the template as the starting state, a greenfield build can run outcome-led from the first sprint - define the endpoint, point at the tests, let the model fill the middle - because the precondition the discipline demands is already standing. Without it, the honest choice is weeks of AI-augmented line-by-line work while the patterns accrete. The template is the difference between those two velocity profiles, and it is decided before the first prompt is written.

Refactors: show the destination

On existing code the bias flips. The model anchors to what is in front of it - and it mimics the local mess exactly as faithfully as it would mimic local cleanliness. Ask it to “clean up” a module and it produces a politer version of the same architecture, because adjectives are not targets. The model cannot converge on “better.” It can converge on that.

So the refactor move is: name the destination. The template is “what good looks like” in executable form - point at it and the task becomes convergence. “This module handles sessions inline; converge it on the transaction manager pattern; mechanical change only.” That task shape is the one LLMs are genuinely excellent at: a shown source, a shown target, and a mechanical transformation between them. No judgment calls hiding inside the diff, review that reads at a glance because every hunk is the same change.

This is Make the Change Easy with the destination made concrete: first the mechanical convergence pass that makes the codebase match the pattern - cheap, reviewable, near-zero risk - then the behavior change lands in a clean zone. Teams that skip the first pass pay for it in the second, where the model improvises against an inconsistent background and every diff mixes movement with meaning.

The honest boundary: this works for mechanical convergence, which is most refactor volume by line count. It does not make the judgment calls - which module is the exception that should not converge, where the pattern itself needs to bend. Those stay human, which is exactly the division of labor the human code budget exists for.

Why not a fine-tuned model

The standard objection: if we want the model to write code our way, why not train it on our code? It is a fair question with a structural answer - the two approaches put your patterns in different places, and the places behave differently.

A fine-tune bakes patterns into weights. The recall is statistical - a tendency, not a guarantee, strongest at the token level: naming flavor, idiom preference, stylistic gravity. What it cannot reliably reproduce is a coherent system - weights do not hand you the transaction manager, the matching container build, and the infrastructure module that deploys it, all mutually consistent and current as of this morning’s commit. And the patterns are frozen at training time: improve your architecture and you are scheduling a training run, evaluating drift, and redeploying a model to ship what a template library ships with a pull request.

A template puts the patterns in context, and context behaves like engineering instead of statistics. It is exact - the model reads the actual current code, not a memory of it. It is versioned - the pattern improves the way all code improves, with a diff and a review. It is inspectable - when output drifts from the pattern you can look at what the model saw and find the gap, which is debugging; when a fine-tune drifts you are doing model evaluation, which is research. And it carries the parts no fine-tune can: the infrastructure, the build, the tests, the documentation stating the contracts.

There is a deeper point underneath. People reach for fine-tuning because it feels like the model is the asset, so the model is where you invest. In template-based engineering the library is the asset, and the model is a renter. Models upgrade every few months; your library survives every upgrade and gets better with every engagement. One of these compounds in something you own.

What a real library looks like

Scale matters here, because the taxonomy claim is a breadth claim. A pattern shown once is an example; a pattern expressed across thirty-plus templates, three backend languages, and the infrastructure underneath is a grammar. Ours covers:

Application templates - runnable services in Java/Spring, Python/FastAPI, and Node/TypeScript, plus React and Next.js fronts, each carrying the same layered architecture: entries at the edge, transactional logic in the middle, repositories below, external adapters quarantined behind DTOs. Same grammar, three pronunciations.

Shared libraries per language - the cross-cutting answers, given once: token verification, structured logging, access-log middleware, configuration loading, the transaction manager above. These are the contracts the application templates assume, which is what keeps thirty templates coherent instead of thirty opinions.

Composition demos - multi-service patterns that show how the pieces assemble: an OAuth token lifecycle service with provider proxies behind an egress gateway, an auth portal in front of an identity service with audit-trail persistence, an admin console pair. These exist because system shape is also a pattern - the model can mimic an integration architecture exactly the way it mimics a function.

Infrastructure modules - the Terraform layer that takes any of the above to a running environment: network, cluster, data stores, messaging, load balancing, DNS and certificates, and identity in three distinct shapes (federated SSO, simple user pools, machine-to-machine). The template story does not stop at the application boundary; “complete” includes deployed.

The live, grouped version of this list is on our engagement page - it is the stack we actually start engagements from, not a brochure inventory.

Prompting against a library

The prompts get shorter as the library gets better, which is the tell that the investment is working. You stop describing architecture in prose - the worst possible medium for it - and start pointing at executable answers. Three shapes cover most of the work:

prompting patterns
# Greenfield - seed first, then ask for outcomes
"Start from the service template in this repo. Add a /api/orders
domain: model, repository, service, routes. Follow the existing
layering and the transaction decorators - write path is
@transactional on the service, read path is @with_session on
the routes. Acceptance tests are in tests/orders_test.py."

# Extension - name the exemplar
"Add a provider proxy for the new vendor. Mirror the shape of
the existing provider proxy pair: token lifecycle handled by
the oauth service, outbound calls through the egress proxy,
request/response DTOs in external/resource. Do not invent a
new integration shape."

# Refactor - name the destination, not the adjective
"Persistence in this module is hand-rolled per route. Converge
it on the transaction manager pattern from the template:
@transactional write paths, @with_session read paths, no inline
session handling. Mechanical change only - no behavior changes.
List the files you will touch before you start."

Notice what is absent: no paragraphs explaining what layering means, no style guides restated per prompt, no hoping the model remembers an instruction from forty turns ago. The architecture lives in the repository where the model can read it, the prompt carries only the delta, and the persistent rules - the ones in your AGENTS.md - enforce the boundaries that must hold even when nobody pointed at them.

One discipline note that earns its keep: when the model proposes a shape that deviates from the template, the deviation is the review item. Sometimes it found a real gap - the template never answered this question, and now you extend the library. More often it drifted toward the ocean - and the correction is one line: follow the template. Either way the conversation is about the pattern, not about this Tuesday’s code, which is how the library keeps getting sharper while the day’s work ships.

What it buys, compounded

Each individual benefit is modest. Together they compound, because they all push the same variable: how much autonomy you can safely grant.

Review gets cheaper - diffs against a known shape read at a glance, and deviation is visible because there is something to deviate from. Drift slows - every service born from the template adds pattern mass that pulls subsequent generation toward the grammar. Onboarding collapses for humans and models alike - the template is the worked answer to “how do we do things here,” which is precisely the question an LLM cannot route around with a hallway chat. And validation boundaries strengthen - the template ships with its test structure, so outcome definitions have somewhere to land.

In the error-budget terms of AI Code Nines: autonomy is purchased with validation, and a template library lowers the price. The same nine costs less supervision when the model is instantiating a verified architecture than when it is inventing one - which means the dial goes higher, sooner, on more of the work, without the quality burn that usually pays for it.

The position, stated plainly: a model upgrade improves every engineering team in the world by roughly the same amount. A template library improves only yours - and it keeps improving every time an engagement teaches it something, survives every model generation, and is owned by you rather than rented from a vendor. If you have budget for exactly one AI investment this year, this is the one we would tell you to make.

Also read

Value Stream Mapping for Product DefinitionThe Quality Drawdown
See the engagement stackLet's Talk

Don't take our word for it - ask ChatGPT what it thinks of this piece.