DEV Community

Ian Johnson
Ian Johnson

Posted on • Originally published at tacoda.Medium on

Explicit Seams as Agent Affordances

The agent’s diff was clean. Three files, all tests green, nothing obviously wrong in the PR. The problem was one of those three files: a service class with no interface in front of it, called from six places, and the agent had quietly changed the semantics of a method in a way that broke a workflow nobody had a test for. The agent didn’t know the workflow existed. The codebase didn’t tell it. The agent saw a function, read its body, and rewrote it.

A week later I rewrote the same code with a port in front of it. Same task, same agent, same scope. The diff came back smaller and visibly safer. The seam was the entire difference.

The usual argument for hexagonal architecture is about testability; ports let you exercise units in isolation. That argument is true and well-rehearsed. The agent angle is its own thing. Explicit seams are what make an agent safe to delegate to.

The agent reads what is there

The agent builds its mental model from what it can see in the file. An explicit port — an interface, a dependency injection point, a named boundary — reads as a contract. A bare function call reads as implementation detail you can rewrite.

The difference isn’t about agent intelligence. It’s about what the codebase signals. A function call says this is mine; change it if you need to. A port says this is shared; other callers may depend on the contract.

The agent respects signals it can see. It does not respect signals it can’t. A workflow depending on an implicit contract is a workflow the agent will break, because the contract is invisible to anything reading the file.

Name the boundary

The most useful idea I took from the hex-architecture work wasn’t the architecture itself. It was the discipline of giving the boundary a name.

A port is a named thing. It has a file, a name, and a list of methods. The name says what the boundary is for.

UserRepository is a port. db.query(…) is not. Same plumbing, different signal. The agent reading code that uses UserRepository knows it’s calling across a boundary. The agent reading code that uses db.query(…) thinks it’s calling a utility.

The naming move is the cheap part. You don’t have to commit to full hexagonal architecture to get the agent benefit. You name the boundaries that matter: the ports that protect cross-cutting concerns like auth, persistence, external APIs, notifications. The agent reads the names and treats them as load-bearing, because the name is the signal that they are.

The other half: name the port for the use case, not the technology. UserRepository is good. PostgresUserStore is worse, because it leaks the implementation into the name. The agent reading PostgresUserStore will reason about it as Postgres code with Postgres-specific assumptions. The agent reading UserRepository will reason about it as a contract. That’s the difference you want.

Where seams pay back on agent work

A few places where the seam visibly changes the agent’s behavior:

Refactors across the boundary. Without a seam, a refactor of an internal helper becomes a refactor of every call site, because the agent doesn’t know the call sites are downstream of an invisible contract. With a seam, the refactor stays on one side. The contract holds; the implementation moves; the call sites don’t change.

Test setup. The agent writing tests for business logic that lives behind a port can mock the port and test the logic in isolation. Without a port, the agent has to stand up the whole dependency chain, often by mocking the database driver directly, with all the brittleness that implies. The first set of tests is honest. The second is mostly mocking glue.

Replacing implementations. “Swap the email provider” is a one-day task in a codebase where the email port is explicit. It’s a multi-day task in a codebase where every place that sends email talks to the SDK directly. The agent does both jobs, but the second one is much more likely to introduce subtle behavior changes, because every call site becomes its own micro-rewrite.

Onboarding the agent to a new area. When the agent is working in a module it hasn’t touched before, the ports give it a map. The agent reads UserRepository, EventBus, BillingClient and understands within a couple of minutes that this module talks to persistence, events, and billing. Without the ports, it reads function bodies and reconstructs the same map by tracing call chains. The seam saves ten minutes per task, every task, forever.

The discipline that isn’t architecture

The valuable shift was realizing that “explicit seams” isn’t the same as “hexagonal architecture.” The architecture is one way to get the seams. The discipline is portable to codebases that aren’t hex-shaped.

Three habits cover most of the benefit:

Every external dependency has a name. The HTTP client, the database, the message bus, the file system. They get names that describe what the application uses them for. The agent reading the code knows these are boundaries.

Every cross-cutting concern has one entry point. Authentication, authorization, logging, feature flags. The agent never has to wonder which path through the code is the canonical one. There’s one, and it’s named.

Every module owns its surface. The functions a module exports are the functions other modules call. The internal functions stay internal. The agent touching one module can see what it’s allowed to change without breaking the others.

Those three habits give you most of the agent benefit of hex architecture without the architectural commitment. The codebase that has them is safe to delegate to. The codebase that doesn’t, isn’t.

What it looks like without seams

I worked in a codebase last year that had grown for ten years without anyone enforcing the seams. Every service called every other service directly; most had mixed concerns and rampant duplication. The auth check was implemented in 17 places, mostly correctly. The database driver was instantiated in two dozen files.

The agent could write features in that codebase. It couldn’t refactor in it safely. Every diff that touched anything load-bearing had a small chance of breaking a call site nobody had remembered. The team’s review burden on agent PRs was enormous, because the agent’s confidence in its diff wasn’t warranted. The agent did not know what it did not know.

The result of this was code we could not prove to be correct, could not trust, and could not ship without a massive QA effort because of the sprawl of features was so interconnected. And it was manual.

The cheapest seam that still counts

The cheapest version of a seam is a typed interface with a single implementation. Two files, ten lines each. The interface names the contract; the implementation lives behind it; the rest of the code talks to the interface.

You can add that to existing code in maybe an hour. The first refactor pays back the hour. Every subsequent refactor pays back the hour again. Small cost, permanent return.

A cheaper version, still useful: a wrapper function with a name. currentUser(req) instead of req.session.user. sendBillingNotification(…) instead of mailer.send({…}). The function doesn’t enforce a boundary the way an interface does, but it names the operation, which gives the agent something to grab onto. The wrapper is a partial seam. Better than nothing.

The thing not to do is wait for the right architectural moment. The seams pay off when they’re in place. Adding them incrementally, even imperfectly, beats not adding them.

The seams help the humans too

The same boundary that lets the agent refactor safely lets a human refactor safely. The named ports that give the agent a map of the codebase give a new engineer a map of the codebase.

The side effect of optimizing for the agent is that you’re also optimizing for everyone else. The codebase becomes easier to reason about, easier to test, easier to change, for any reader. The agent is just the most literal reader, so the agent benefits most visibly. The human benefits are slower but real.

That’s the case I’d make to a team that’s skeptical about doing architectural work “for the agent.” The architectural work is good architecture. The agent is the proximate motivator. The codebase ends up cleaner regardless.

Add one seam this week

If your codebase has no explicit seams, here’s what I’d do tomorrow morning.

Pick the boundary that costs you the most when it breaks. The auth check, the database, the payments client, the email service. Whichever one has the most call sites and the most consequence when it goes wrong.

Add a named interface and a single implementation behind it. Move the call sites one at a time. Five a day until they’re done. Don’t try to do all of them in a week.

The first refactor in the seamed area will surprise you. The agent will produce a smaller, safer diff than it produced in the same area a month earlier. The diff will land with less review back-and-forth. The reviewer will spend their attention on the logic instead of the cross-cutting checks.

Repeat with the next-most-painful boundary. The codebase that emerges is one the agent can move in safely. The team can move in it safely too, for the same reasons.

The seams are agent affordances. The agent does the work the codebase makes possible. Make more work possible.

Top comments (0)