Back to Engineering
Seven concerns for building B2B SaaS when agents write the code
This summer Robert C. Martin (Uncle Bob, the author of Clean Code) posted this:

Robert C. Martin (@unclebobmartin), July 23, 2026.
I’m significantly older than you. I started coding in the late 60s. My current strategy is to not read any of the code written by my agents. That’s the only way I can take advantage of their productivity. What I do instead is to surround the agents with extreme constraints. Unit tests, gherkin tests, QA procedures, quality metrics, mutation testing, test coverage, and a plethora of others. In the end, I have very high confidence in the code they produce because they’ve had to run the gauntlet of all of my constraints and tests.
If you’ve been writing software for a while, that quote should land somewhere uncomfortable. This is a man who spent a career arguing that code should be read, named well, and reviewed with care. His current strategy is to not read it.
If the author of Clean Code isn’t reading the code his agents write, what exactly is my job now?
I have my own version of that discomfort. For years I required that everyone on my team review their peers’ pull requests line by line. It was how we caught bugs, and it was how junior engineers learned the codebase from people who knew it. Today, in my own org, we manage agent sessions and “review” pull requests, often without a consistent shared practice for what that means. Tomorrow it may be operating fleets of agents that move work from product idea to pull request while we’re in a design review, stepping in only when something needs a human decision.
The volume of code any of us is responsible for has already outgrown the hours we have to read it, and that gap is only going to widen.

Illustrative only. The actual split between prompt, review, and harness time varies by team, by org, and by how far along this shift you are.
Back to engineering
Here’s the reframe I’ve settled on: this isn’t the end of the craft. It’s a return to it.
For most of my career, engineering as a day-to-day activity was crowded with things that had little to do with engineering. Typing out boilerplate. Fighting a test framework with its own opinions about how you should structure code. Debugging the unit-test framework instead of the system it was supposed to test. Trading away a cleaner design because the deadline wouldn’t move, and design was the thing nobody outside engineering could see losing.
Design and judgment were always the actual job. They were just fighting for oxygen against a mechanical layer that ate most of the week.
That layer is now cheap. What’s left, once you strip it away, is the part that was always the “engineering”: deciding what the system should be, stating the invariants it has to hold, designing the constraints that keep it holding them, and proving that it does. I’ve come to think of this as a renewed responsibility rather than a diminished one: something to own, not something automated out from under you. The old source of confidence (I read every line, so I know it’s right) doesn’t survive the volume we generate now. The new one has to come from somewhere else, and finding it is the work.
The trap
Even before AI, we could never afford to mistake “the code got written” for “the system is sound.” Now the difference between the two is starker, and understanding it matters more than ever.
Is the code correct? Will it hose the database? Can a bad actor get at customer data? An easy-going posture, one that treats a green CI run and a clean diff as the whole verdict, will wave all of that through. None of it announces itself. It just accumulates.
The economics explain why this is a live risk. When a teammate wrote 400 lines over three days, reading all 400 lines was feasible and, frankly, respectful of their work. When an agent writes 4,000 lines over an afternoon across three services, “read everything with equal care” stops being a strategy anyone can execute. You skim, you approve, and you feel vaguely guilty about it: review fatigue. Output has outstripped the reader, and the reader has no frame for where the risk actually lives.
The Google engineering practices guide is still one of the best things ever written about code review, and what it says review protects (design, functionality, complexity, tests, naming) hasn’t changed at all. What changed is the economics of applying it. The old wisdom is still correct; it just can’t be the only tool.
Three places judgment enters the system
If you can’t read everything, what do you say before generation, what do you read after it, and what do you automate? Your engineering judgment now enters the system in three places.

The three places engineering judgment enters an AI-generated system.
The prompt. Every session is requirements engineering. The context, constraints, and invariants you state up front decide most of what gets generated. Say a concern out loud before the agent writes anything, and the violation often never exists at all.
The review. Still essential, but targeted, because reading everything with equal care stopped being possible. Reviews spend their time on the concerns most likely to be in play for the surface being touched.
The harness. The rules, checks, and gates that do the supervising when you’re not looking: your conventions file, an architecture-reviewer agent, testbeds, CI gates, migration linters, the load and stress environments that test how things fail. This could be the culmination of all your engineering experience applied, because a harness distills every catch you’ve ever made into something that catches it automatically from now on.
This is where I want to come back to Uncle Bob, because I think he’s more right than the discomfort of that tweet first suggests, and incomplete (because it’s only a tweet). He’s right that you surround agents with constraints and let the gauntlet do the work you used to do by reading. But constraints about what? A gauntlet of unit tests, gherkin tests, and coverage numbers is a floor, not a bar. Does asking an agent to generate two thousand tests actually deliver value, or just make the floor look taller? The constraints need a shape, organized around the properties of the system that actually matter, not around whichever test is cheapest to generate.
That shape is what the rest of this piece is about, and there’s a practice buried in it worth naming on its own: caught it twice? Encode it. A catch you encode is a catch you never have to make again, by hand, under time pressure, at 4,000 lines an afternoon.
Why B2B SaaS is its own case
Not all software has the same concerns, and a generic list of quality attributes doesn’t serve anyone well. Being specific about the kind of software matters.
For a lot of consumer software, you could, at least in theory, write a complete spec: a fixed set of endpoints receiving and returning a consistent shape of data that has to scale to some number of requests per minute. The spec is large, but closeable. In B2B SaaS, every customer brings their own workflows, integrations, permissions, data, and configuration. The combinations are the product. The spec is never finished, because the next enterprise customer is a new set of requirements.
This isn’t a hunch. To understand where things actually go wrong, I bucketed our corpus of bugs, issues, and review findings across categories and subcategories. The concerns in this piece are what fell out of that exercise, weighed against my own experience of what it takes to keep a B2B system healthy.
A few other things fall out of building for many businesses on one system, rather than one product for many individuals:
- (Multi-tenant) Many tenants share one system. Tenant isolation and IDORs are the default risk, not an edge case. White-labeling means nothing hardcoded per client, including the things you’d never think to check because they were true for every customer you’d tested against, until they weren’t.
- Third-party integrations. Correctness depends on the third party’s actual semantics, not its documented ones. An agent that has read the docs of every API in the world remembers them imperfectly, which is wrong enough to be dangerous.
- Your customers’ revenue depends on your error states. A failure mode that reads as a minor inconvenience to you is someone else’s business day.
- Long-lived contracts with consumers you don’t control. API clients, webhooks, and mobile binaries already in the wild will keep calling last quarter’s contract whether or not you’ve moved on.
The map: surfaces and concerns
The same diff means different things depending on what it’s touching. I organize what we build into three surfaces: full-stack web (frontends and their presentation layers); mobile (the app plus everything that ships it: the OTA-updatable JS layer, native modules, release trains, push and deep-linking infrastructure); and distributed systems and services (subgraphs, gateways, jobs, queues, lambdas, data pipelines, anything moving data from one store to another).
Adding a relation between two entities is a schema tweak in an internal tool, but on a content-heavy surface it’s a read-path decision that has to be optimized for reads from day one. Changing a GraphQL query’s shape is a refactor on web, but on mobile it’s a contract change with every binary already in the wild, because last quarter’s app version still sends last quarter’s query.
In practice, surfaces have mattered less to me than I expected going in. They ground the framework so it isn’t purely theoretical, and they change what “check it” means for a given concern, but they’re not where most of the interesting judgment calls live. The concerns are.
The concerns are what property of the system a change threatens: seven of them, each with a one-line test you can hold in your head.
| Concern | The one-line test |
|---|---|
| Correctness | Is the spec itself right: the real problem, the third party’s actual semantics, a solution that matches what the code actually does? |
| Maintainability & extensibility | Can the next change be made safely and cheaply? |
| Security | Can only the right principals do or see this? Have we identified the actors? |
| Performance | Is the access pattern right for the read/write shape, and does it survive N× load? |
| Reliability & failure modes | What happens when a dependency is slow, down, or half-done? How do our error states affect customers? |
| Testability & verifiability | Can this code prove itself correct without a human reading every line? |
| Operability | Can the team, and its agents, run this? Do we know when something is actually failing, and can we fix it in minutes? |
Put the two dimensions together and you get a map. Every diff sits in at least one cell, and the same concern asks a different question depending on the surface:
| Concern | Full-stack web | Mobile | Distributed systems & services |
|---|---|---|---|
| Correctness | Does the UI enforce the real business rule, not the ticket’s paraphrase of it? | Does the behavior hold for app versions already in the wild? | Does the integration match the third party’s actual semantics? |
| Maintainability | Is this a shared component or a one-off, and is it built like the one it is? | Does this belong in the OTA-updatable JS layer or in a native module? | Is coordination logic staying out of the domain it’s coordinating? |
| Security | Is every read scoped to the tenant, with nothing hardcoded per client? | Are tokens, deep links, and push payloads treated as untrusted entry points? | Is authorization checked against the entity’s owner, never an ID the caller supplies? |
| Performance | Is the read path shaped for content-heavy pages? | How many round trips and how big a payload, on a real mobile network? | How big does N get for your largest tenant? |
| Reliability | What does the customer see when a dependency is down? | Can the app survive a bad release you can’t roll back instantly? | Is every handler idempotent under at-least-once delivery? |
| Testability | Do the tests assert behavior a user depends on? | Is it verified across the OS and app versions you still support? | Are there contract tests, and does anything test the failure paths? |
| Operability | Can you trace a customer’s broken page to its cause? | Can you see crashes and adoption by release? | Are logs queryable, alerts tied to failure modes, and runbooks followable by an agent? |
Surfaces across the top, concerns down the side. Name the cell before you open the diff.
The seven concerns
Each of these concerns gets its own post in the coming weeks. What follows is the shape of each one: the test, how agents get it wrong, where it lives in the prompt, the review, and the harness, and a glimpse of what it looks like in practice. Consider it a preview, not the full treatment.
These seven aren’t a universal taxonomy. They overlap with classic quality-attribute lists, and I’ve borrowed freely where those already got it right. What makes this set specific is that it’s drawn from what actually generated review comments and incidents in my own B2B SaaS experience, not from first principles. Accessibility, compliance and data residency, and cost all matter; they just weren’t where review energy or incidents concentrated for us. Cost (infrastructure and LLM spend both) is folded into performance and operability rather than standing alone. Software engineering is a contextual craft; this list is useful because it’s grounded, not because it’s exhaustive.
Correctness
The test: Is the spec itself right: the real problem, the third party’s actual semantics, a solution that matches what the code actually does?
In my experience this is the concern agents are worst at, and confidently so. Feed a session an ask aimed at a symptom rather than the real problem, and you get a beautifully engineered symptom patch. Feed it an ask that misunderstands a third-party system, and you get a beautifully engineered reimplementation of something that system already does. An agent optimizes the ask it’s given; it rarely asks whether the ask deserves to exist, because that requires knowing what the business actually needs, and pushing back on a well-formed request isn’t in its instincts unless you’ve put it there.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “For any third-party API you touch, quote and link the relevant doc line. Do not reimplement anything the vendor already provides.” |
| Review | Does the premise survive contact with what the third party actually does, not a plausible narrative about it? |
| Harness | A subagent checks vendor semantics against the docs before a design is approved. |
In practice. The most expensive correctness failures I’ve seen weren’t bugs in the code. They were careful, well-tested, already-approved implementations of the wrong premise: a feature the vendor already provides, a fix aimed at a symptom instead of the cause. Nothing in the diff tells you. You have to know, or check, what the business and the third party actually need.
Maintainability & extensibility
The test: Can the next change be made safely and cheaply?
Agents duplicate rather than extend, because a second implementation is cheaper to produce than finding and understanding the first, and search sometimes fails or context runs out before it finds what already exists. They don’t reliably distinguish a one-off script from a module meant to be extended, so a throwaway pattern gets treated as the convention to follow next time. Coordination logic accretes wherever the action already is, because adding one more side effect to a method that’s already doing six things is the path of least resistance for a single diff, even though the tenth side effect turns the method into a coordination hub nobody meant to build.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “Search for an existing implementation first. If you replace something, delete it in the same PR.” |
| Review | Name the widest boundary this diff couples across, and ask whether the coupling earns it. |
| Harness | A cohesion and coupling check on every PR, thresholds ratcheting over time. |
In practice. It usually shows up as a second, slightly different implementation of a rule that already exists somewhere else. Each one is fine on its own. Together they’re two sources of truth, and the next change fixes only one of them.
Security
The test: Can only the right principals do or see this? Have we identified the actors?
The failure mode I trust least is the vacuous authorization check: code that derives a company from the caller’s own token, then “verifies” the caller belongs to that company. It has an authorization call on the screen, so review slides past it, and it can never fail, since it’s checking the caller against something built from the caller. IDORs and unauthenticated endpoints round out the list, and agents are prone to all three because each one looks, at the diff level, like a reasonable check that happens to be present.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “White-label surface, nothing hardcoded per client. Authorize against the fetched entity’s owner, never an ID the caller supplies.” |
| Review | What does this check actually compare, and what happens if it throws instead of returning false? |
| Harness | A static IDOR check on every PR, plus a required negative test on any authorization change. |
In practice. The authorization checks that worry me most are the ones that are present. A check that exists, runs, and compares the wrong things reads as covered in review. It’s usually caught only by someone who deliberately traces the malicious path.
Performance
The test: Is the access pattern right for the read/write shape, and does it survive N× load?
Agents produce plausible-looking N+1s constantly, because every individual query in the chain is correct and fast in isolation, and the defect only exists in the aggregate count. Concurrency makes it harder to spot, because a parallel fan-out reads like an optimization. In JavaScript, for example, it looks like this:
// Reads as "fast, in parallel." Issues one query per invoice.
const customers = await Promise.all(
invoices.map((invoice) => customerRepo.findById(invoice.customerId)),
);
The same shape exists in any language with a concurrency primitive. The question nobody asked is how big N can get. No one does the arithmetic for N× load unless asked, because an agent has no sense of your production cardinality; it only has the fixture in front of it.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “Reads go through the batching layer (a dataloader, in GraphQL terms). State the cardinality at our largest customer before writing the query.” |
| Review | How big can N get, and is that fine? Ask for the number, then price the risk. |
| Harness | A required timeout on outbound calls, a required limit on lists, a query-count assertion on data-access paths. |
In practice. The classic shape is a fan-out that’s fast against the test fixture and slow against your largest customer, sometimes with a comment calling it an optimization. The review that catches it is a single question: how big does N get?
Reliability & failure modes
The test: What happens when a dependency is slow, down, or half-done? How do our error states affect customers?
Agents write the happy path first and most confidently, because that’s what the ticket describes and what the test fixture exercises. Handlers that assume they’ll be called exactly once, when the real delivery guarantee is at-least-once, are a recurring pattern: a retry or redelivered message replays a handler never written to tolerate being replayed, and the visible symptom, a duplicate notification, a double-charged action, is usually several layers downstream of the actual defect, a half-done state nobody designed for.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “This handler must be idempotent; assume at-least-once delivery. The queue may hand you the same message twice.” |
| Review | What does the caller see when this dependency times out, recoverable state or a stranded customer? |
| Harness | A failure-injection environment that kills a dependency mid-request and checks what state is left behind. |
In practice. A completion handler that isn’t idempotent gets a late or duplicate “finished” signal, rewrites state that had already moved on, and the visible bug (a repeated notification, a re-triggered step) looks like a message deduplication problem. The actual fix is almost always making the handler refuse to act unless the state it expects to find is still there. Adding a dedup key to hide the symptom papers over the mechanism instead of fixing it, and the system gets more layered, not more correct.
Testability & verifiability
The test: Can this code prove itself correct without a human reading every line?
An agent asked to add tests will happily generate a large number of them, and coverage will go up, and CI will stay green, without any of it proving the behavior actually works. The failure mode is tests that assert what the implementation does rather than what it’s supposed to do: a helper gets built, a comprehensive test file gets built to exercise it, and the helper’s only caller in the entire codebase turns out to be the test that was written to prove it works. Passing tests are the floor. They are not the bar.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “Write the test that would have caught this bug, not one that proves the happy path exists.” |
| Review | Does this assert an observable behavior a caller depends on, or an implementation detail true only today? |
| Harness | A CI gate requiring a behavioral test change on any diff touching authorization or state transitions. |
In practice. A bug fix ships with a large, well-structured test file, coverage climbs, and a later reviewer notices the only thing calling the new code path is the test itself. The tests are real, they pass, and they verify nothing a user will ever encounter.
Operability
The test: Can the team, and its agents, run this? Do we know when something is actually failing, and can we fix it in minutes?
The question isn’t only whether a person on the team can operate what shipped. It’s increasingly whether an agent, handed an incident or an on-call page, could follow the same path a human would: read a log, find the failing dependency, understand what to do next. Logs nobody can query and the absence of a runbook are both operability failures, and they’re the kind an agent inherits along with everyone else, because an agent debugging your production system is limited by the same observability gaps a human is.
Prompt / Review / Harness
| Where | What it looks like |
|---|---|
| Prompt | “This needs a runbook entry: what fails, how we’d notice, how we’d fix it in minutes, written so an agent could follow it.” |
| Review | If this fails at 3am, what does the first responder, human or agent, actually see? |
| Harness | Structured, queryable request logging on every privileged action, alerts tied to the failure mode, not a generic error rate. |
In practice. A service ships with logging designed for debugging, capturing the happy path and little else. When it fails in production, the log line that would explain why doesn’t exist, because nobody wrote it for a failure that hadn’t happened yet. That gap surfaces at the worst possible time: the first real incident.
Where review energy actually goes
Looking back over a year of our own code and architecture reviews at my current company, a shape emerged that I don’t think is unique to us.
Over half of all review energy goes to maintainability: structure, coupling, duplication, boundaries. Even with better models, we still have to keep deciding what is a script and what is meant to be extended, and how, and that decision doesn’t get any easier just because the code arrived faster.
Security feedback is rare, but it’s never shallow. There’s no such thing as a routine security comment. When it shows up, it’s an IDOR, a vacuous authorization check, or an unauthenticated endpoint, and it demands the reviewer’s full attention every time.
Performance arguments are won with arithmetic or a dashboard link, almost never with opinion. “That seems slow” doesn’t move a review. “This is N+1 against a table that grows with every customer we sign” does.
The lesson I draw from that shape is to calibrate depth by concern rather than spreading attention evenly across all seven, because they don’t arrive with equal frequency or equal cost when missed.
Practices
A few guidelines have held up well enough that I keep coming back to them, borrowed and generalized from what’s worked at my own company.
- Before you open a diff, name the cell. Which surface, and which two or three concerns is this change most likely to hurt? Ten seconds of framing beats twenty minutes of unfocused scrolling.
- Front-load concerns into your prompts. Before you send an agent off, ask which of the seven concerns this task touches, and whether you’ve said so. One sentence of constraint up front is the cheapest quality control there is.
- All seven deserve judgment; calibrate by surface. Security and reliability get depth every time they’re in play. Maintainability gets the boundary questions (right module? right contract?) on every diff, because it’s the one that shows up constantly rather than occasionally.
- Passing tests is the floor, not the bar. Does asking an agent to generate two thousand tests actually deliver value, or does it just make the floor look taller?
- Root-cause output that falls short, and encode it. A repeated catch is a harness rule waiting to be written: a line in your conventions file, a lint rule, an architecture-reviewer check. This is how you scale your own judgment past the hours you have to spend applying it by hand.
The job came back
The fear at the start of this piece was that the job I love went away. I don’t think it did. I think it came back.
For most of the history of this profession, the craft we actually wanted to practice, deciding what a system should be and proving it holds up, was competing for time against typing, against fighting tooling that should have gotten out of the way, against deadlines that made design the thing that quietly lost. Some orgs drew lines between application developers, software developers, and software engineers, and those lines came with their own pain points. That competition is mostly over. What’s left is the part that was always the point.
So here’s the question I’d leave you with, the same one I ask myself. Think of the best review comment you’ve ever received. Which of the seven concerns was it protecting? And where would it live today: in your prompt, in your review, or in the harness?
Over the coming weeks I’ll go deeper on individual concerns, one at a time.