AIsolation of Slop

A model can now write a working service faster than you can read it. New tools bring new workflows, and for most of us they are already changed how software gets built. It is only natural that the way we architect our applications follows, and adjusts to the new reality.

The economics are changing

For most of the history of programming, writing code was the expensive part. We optimized our tools and our architecture around human working memory because every line had to be understandable by the next person who will work on that code. Today, generating code is cheap. The expensive parts have shifted to reviewing changes, developing for the wrong architecture, security issues, and pulling it back out when production goes sideways. Our jobs is becoming more and more about managing the complexity of the system, rather than about the code itself. This is not to say that the art of writing code has lost its charm, but that in commercial software development it is becoming less and less appreciated, since rewriting something in a different language or framework become a matter of a few prompts.

Its not just the code. AI has worked its way into every corner of software development and IT in general. It writes tickets, drafts documentation, summarizes meetings, prepares presentations, and helps us answer client requests. The real challenge is that it can produce more work than people can realistically review.

As the volume grows, ownership shrinks. More and more, it feels like the person putting their name on a pull request, design document, or ticket never really understood everything that went into it. They skim the diff, the tests pass, the CI is green, and the change gets merged.

We call that slop. The code that appeared almost for free and nobody truly owns it. It may be perfectly correct. It may even be well written. But it spreads faster than understanding does. One service reaches into another, dependencies accumulate, and suddenly a small change becomes difficult to review, risky to deploy, and painful to roll back.

Some companies respond by banning AI-written code or rejecting AI-generated pull requests. For most of us, that is not a realistic option. Deadlines, rising expectations, and constant context switching all push in the opposite direction. By the looks of it, the amount of generated code is only going to increase.

The more likely future is that we'll accept more AI-generated code, spend less time understanding each change, and gradually lose ownership of parts of our systems. In many teams, that future is already here.

Fighting that trend is unlikely to succeed, and after accepting this new reality of software development, the natural next step is to think about how to contain it.

Accept that some code will be generated, merged, and only partially understood. Then design your system so that this is survivable. Give every service a boundary tight enough that a bad change stays inside one folder. Generating, rewriting, or deleting a service becomes a contained event instead of something that ripples through the rest of the application.

This post builds on Keep Calm and Start with a Function, which introduced a simple project structure of models, services, helpers, and consumers built from plain functions with clear inputs and outputs. Here we'll tighten those boundaries for a world where a significant portion of the implementation is written by a machine.

For decades, good architecture helped developers manage complexity. That still matters, but the bottleneck has shifted. Today, architecture is just as much about managing uncertainty. If a service breaks, can you replace it? Can you turn it off? Can you roll it back without touching everything else?

Good modularity minimizes blast radius and maximizes replaceability. Treat services as disposable rather than precious. Spend less effort perfecting what is inside a service and more effort defining its boundaries, its contract, and how you deploy, replace, or roll it back.

Isolation over elegance

None of this is new. Reviewing code is a lot harder when a change is not contained. As the team at SoftwareMill put it, when a change bleeds across the codebase, reviewing it becomes shotgun surgery. When the change is contained, a reviewer can hold the whole thing in context and make a real decision about it. The tighter the boundary, the smaller the thing a human has to trust. The good part is that we have decades of prior art. Here are four places the industry already solved a version of this problem.

Django

Django has argued for containment since long before LLMs. Its stated goal is loose coupling and tight cohesion. The layers of the framework should not know about each other unless they have to. The template system knows nothing about web requests. The database layer knows nothing about how data is displayed. The pieces are independent wherever they can be.

The same philosophy runs through Django apps. An app is meant to do one thing, stay self-contained, and be portable enough to drop into another project. That is the property we want from every service. It owns its corner, it does not reach into yours, and you can pull it out without unraveling the rest. It is the older Unix philosophy wearing a framework, small programs that do one thing well and compose through clean interfaces.

Erlang

Erlang made the same bet decades earlier, for harder reasons. It was built in the late 1980s to run telephone switches that could not go down, and its answer was isolation taken to the extreme. An Erlang program is thousands of small processes that share no memory and talk only by passing messages. When one process hits an error, the instinct is "let it crash" instead of defending against every case. A supervisor notices the dead process and restarts it clean, so a fault stays a local fault instead of corrupting the whole system.

Two things fall out of that design. A process can be swapped for a new version while the system keeps running, because nothing else holds a reference to its internals. And a broken process is cheap, because it was small and isolated to begin with.

"Let it crash" and "let it slop" sound awfully similar. You accept that a unit will fail or be imperfect, and you spend your effort making sure the failure cannot spread past its own boundary. The goal is not a system that never breaks. It is a system where breakage is local and recovery is cheap.

Microservices & Event Driven Systems

Microservices pushed isolation all the way out to the deployment boundary. Each service runs as its own process or its own machine, owns its own data, and ships on its own schedule. Services talk to each other over the network through a published API and never by reaching into another service's database. Because a service owns its data and exposes only that API, you can rewrite its internals or redeploy it without coordinating with the rest of the fleet.

The contract is often the API, a schema that is versioned so a change does not break the services that call it. A bad deploy takes down one service, not the system, and you roll that one service back. The price is real operational complexity, since now you run a network, tolerate partial failure, and trace requests across processes. The lesson worth borrowing without paying that whole bill is simple. Give each unit its own data and a published contract, and let it be deployed and replaced on its own.

Event-driven systems go one step further. Services do not call each other at all. A service emits an event when something happens, and other services react to it, so a producer does not know or care who is listening. The coupling moves off the call graph and onto the shape of the events themselves.

That only stays sane if the events are written down somewhere authoritative like an event catalogue, a central record of every event schema. Producers and consumers depend on the catalogue rather than on each other, and a schema registry can reject a change that would break existing consumers before it ever ships. This is the same idea we come to next, keeping the contract outside the code that implements it, applied at the scale of a whole system. The contract lives in one place everyone agrees on, and every part depends on that instead of on the inside of its neighbours.

The service is the unit

Start from the structure in the previous post, made of models, services, helpers, and consumers. The one change here is that a service is no longer a single file. It is a folder that owns everything about one job, including the description of what it does and the shape of what goes in and out.

models/
signup.py # contract: input and output types
billing.py
services/
signup/
__init__.py # the public entry point, thin
signup.py # the implementation, slop lives here
SERVICE.md # what the service does and must not do
billing/
__init__.py
billing.py
SERVICE.md
The folder is the boundary. Everything the service needs to be understood, called, and replaced sits inside it. The one thing that must not sit inside it, buried in the logic, is the definition of its own contract.

Contracts live outside the service

A service's input and output types are its contract with the rest of the system. Keep them as "global" models separate from their service and let the implementation import them, not the other way around. The implementation is the part you expect to throw away. The contract is the part you keep.

# models/signup.py
from dataclasses import dataclass
@dataclass(frozen=True)
class SignupInput:
email: str
name: str
@dataclass(frozen=True)
class SignupResult:
user_id: str
status: str # "active" or "pending"

The implementation depends on those types and nothing else depends on the implementation. Callers speak in SignupInput and SignupResult. What happens between them is the service's business and no one else's.

# services/signup/signup.py
from models.signup import SignupInput, SignupResult
from helpers.validation import is_valid_email
from services.user import create_user
from services.email import send_welcome_email
def process_signup(data: SignupInput) -> SignupResult:
if not is_valid_email(data.email):
raise ValueError("Invalid email")
user = create_user(email=data.email, name=data.name)
send_welcome_email(user)
return SignupResult(user_id=user.id, status="active")
The contract is the stable seam. You can rewrite signup.py from scratch, hand it to a model and ask for a faster version, or delete it and start over, and as long as the new code takes a SignupInput and returns a SignupResult, nothing outside the folder has to change. The types are what callers were coupled to, and the types did not move.

Every service gets a spec

A contract says what shape the data has. It does not say what the service is for, or what it is not allowed to do. That goes in a short markdown file next to the code. It is the first thing a model should read before it touches the service, and the thing a reviewer checks the change against.

# signup
Creates a user account from an email and name, then sends a welcome email.
## Input
SignupInput: email (str), name (str)
## Output
SignupResult: user_id (str), status ("active" | "pending")
## Requirements
- Reject invalid emails with ValueError before any side effect.
- Creating the user and sending the email must both happen, in that order.
- The function is the only public entry point. Import it, do not reach inside.
## Must not
- Talk to the database directly. Go through services.user.
- Import from another service's implementation file.
- Add configuration or state that outlives a single call.
This file does two jobs. It gives the model a small, explicit brief instead of the whole codebase, which is the difference between a focused change and a wandering one. And it gives the reviewer a checklist. The change is good if it honours the spec and stays in the box, and suspect if it does anything the spec did not ask for.

A spec and a reviewer are not the only line of defence. The "Must not" rules are mostly about imports, and imports can be checked by a machine. A tool like import-linter, or an architecture test in your suite, can encode the boundary as a rule, so a service that reaches into another service's implementation fails the build instead of waiting for a reviewer to catch it. It is the schema registry from earlier, brought down to your own code. The boundary is written down, and a build step rejects the change that crosses it.

Refactor by replacement

Once the contract and the spec live outside the implementation, the implementation stops being precious. You do not have to carefully refactor it in place. You can write a new version beside the old one, point traffic at it, and delete the old one when the new one holds up. It is not mandatory, but its an option.

This is the "replace, don't refactor" idea, and it only works because the seam is fixed. The new version answers to the same models/signup.py and the same SERVICE.md. Nothing upstream knows which version it is calling. If the new one misbehaves, you switch back. The blast radius of the whole exercise is one folder.

Refactoring a function you own is cheap. Refactoring a web of cross-service references is not. Keep the references shallow and pointed at contracts, and the expensive kind of refactor never has to happen.

The architect of the shell

There is one part of the system you do not hand to the machine easily. Call it the shell, the main app, the orchestrator, the composition root, the thin layer that wires the services together and decides what calls what. In the previous post this was the consumers layer, the routes and workers that deserialize a request, call a service or two, and serialize the result. It is small on purpose, and it should stay that way.

The rule for a service is that you can afford not to know every line of code by heart, because the contract and the spec fence it in. The rule for the shell is the opposite. You own it completely. You should know every call it makes, why that call is there, what happens when it fails, and what order things run in. A service is disposable. The shell is not, because the shell is the thing that contains everything else, and a mistake there is the mistake that is not contained.

The shell owns the services. It should be minimal, understandable, and documented, and it deserves far more of your attention than any of the isolated services beneath it. A service you can rewrite next week. The shell is what holds them together, so it is the part you keep clean and know by heart.

Most of the real work here is a judgment call no model can make for you. Where does a service end and the shell begin? What is one job worth isolating, and what is two jobs pretending to be one? Should a piece of logic live inside a service, behind its contract, or out in the shell where the wiring can see it? Draw the line in the wrong place and you get services that have to know about each other, or a shell that has quietly grown another brain and become the slop it was meant to keep out.

That thinking spans from software architecture to system design.

Reviewing AI-generated code

When a model produces more code in an hour than a team can read in a day, the bottleneck moves from writing to reviewing. A lot of the value a developer adds now comes from reading a change well and judging whether it is correct and whether it should ship, rather than from typing it out in the first place.

Isolation is what makes that job tractable. Reviewing a single self-contained service is a bounded task, since the reviewer reads the spec, reads the contract, and confirms the change stays inside the folder and does what the spec says. Reviewing a change that propagates through several services is a different order of difficulty, because the reviewer has to hold every affected path in their head at once. The first fits in working memory. The second does not.

The same boundaries pay off across the rest of the work. Writing the spec is easier when the service does one thing. Copying a service into another project is easier when it carries its own contract and depends on nothing around it. Testing gets easier too. A service that speaks only through its contract can be exercised through that contract alone, with its neighbours stubbed at the seam, so a test never has to stand up half the system to check one job. The cognitive load of each task drops, and the speed at which a team can safely ship changes with it.

Isolate the slop

You are not going to out-type the machine, and you do not need to. Let it write the inside of the services you build. Keep the contracts, the specs, and the boundaries under your ownership, and a bad service stays a bad service instead of becoming a bad codebase.
Cheap code is fine, as long as it is caged. Relax, and isolate the slop.