Back to Blog
The Hidden Token Tax of Beautiful Abstractions

The Hidden Token Tax of Beautiful Abstractions

By Steve Ruben
AIAI AgentsSoftware ArchitectureBest PracticesInnovation

We've spent decades getting really good at building abstractions. Repository patterns, fluent builders, custom DI conventions, attribute-driven behavior, the hallmarks of a well architected codebase. Systems designed so that junior developers couldn't easily do the wrong thing, and senior developers didn't have to repeat themselves. Beautiful, consistent, invisible machinery humming underneath the surface.

Then AI coding agents arrived, and the machinery stopped being invisible.

Picture this: you ask an agent to add rate limiting to an API endpoint. The codebase uses a custom [ThrottlePolicy] attribute system, built by a well intentioned architect to centralize rate limit configuration across 40+ controllers. Elegant. Consistent. Completely opaque to the agent.

It produces six files of confident, well formatted, completely wrong code. It invents a ThrottlePolicyRegistry class that doesn't exist, wires it into the middleware pipeline in the wrong order, and decorates the new endpoint with an attribute signature it fabricated wholesale. Everything looks right. None of it works. Twenty minutes of archaeology to untangle what it did and why.

That's the token tax. Every layer of custom abstraction your agent can't reason about doesn't just slow it down. It sends it down a confident, plausible, wrong path. And wrong but confident is the most expensive kind of mistake.

The Old Contract

For most of our careers, abstractions were a clear win. Keystrokes were expensive, not in dollars but in time, cognitive load, and review cycles. A well built repository pattern meant you wrote one base class and your team got consistent data access across 30 services. A custom DI convention meant nobody had to remember to register their services. A fluent builder kept complex configuration readable and enforced invariants at construction time.

The math made sense: invest effort once, save effort many times over. We built frameworks on top of frameworks. We wrote [AutoRegister] attributes, custom IEntityTypeConfiguration base classes, fluent pipeline builders with private backing state. We called it clean architecture. We called it elegant. In code reviews we called it the right way to do it.

The contract was: complexity upfront, simplicity forever after.

It was a good contract. For a long time.

The Cost Center Moved

Here's what changed: keystrokes are now free.

Not cheaper. Free. An agent will generate a hundred lines of correct, idiomatic, well commented code in the time it takes you to open a file. The cost of writing code has effectively hit zero. Which means the old justification for abstraction, amortizing keystroke cost across future features, is gone.

The new bottleneck isn't writing. It's comprehension. Specifically, the agent's ability to correctly interpret your codebase and produce an accurate result on the first attempt. That comprehension costs tokens, and complexity is now the tax.

Every custom abstraction layer adds to that tax:

  • Indirection: the agent has to trace the execution path before it can reason about behavior
  • Convention magic: the agent can't observe what the convention does, only where it's applied
  • Custom DSLs: the agent has to infer the grammar from usage examples, not from documentation
  • Fluent chains with private state: the agent sees the surface API but not the invariants it enforces

None of this was a problem when you were the one writing the code. You built it. You know how it works. The agent doesn't. Sometimes it asks. Sometimes it assumes and charges ahead. But even when it does ask, it can only ask about what it can see. Even when it does ask the right questions, you can't guarantee the context window has room for everything it actually needs. A context window is a finite budget. The more complex your abstractions are to explain, the more of that budget gets consumed just establishing what exists and how it works, leaving less room for the agent to actually reason about what it's building. You end up in a situation where the agent technically has access to your architecture but doesn't have enough headroom to think clearly about it at the same time. It fills the gap with inference, and produces code that looks complete but is built on a foundation it never fully understood.

Your Abstraction Is an Undocumented Library

Here's the insight that made this concrete for me: a custom abstraction is just a library nobody bothered to publish.

Think about what happens when an agent needs to use Entity Framework. It has access to years of official documentation, hundreds of Stack Overflow threads, countless blog posts, and a significant chunk of its own training data covering EF usage patterns. When it writes dbContext.Set<Product>().Where(p => p.IsActive).ToListAsync(), it's drawing on a deep, well-indexed body of knowledge. It gets it right.

Now think about what happens when it needs to use your internal DataContextFactory<T> wrapper that you built in 2022 to normalize multi-tenant query scoping. It has your XML doc comments if you wrote them. It has whatever's in your internal readme, if that exists, if it's current. It has usage examples scattered across the codebase that it has to pattern-match against. That's it.

The agent hits your abstraction and it cannot Google it. It reverse-engineers behavior from call sites and makes inferences. Sometimes those inferences are right. Often they're plausible but wrong in a way that only surfaces under specific conditions, the kind of bug that slips past code review and lands in production.

Same story with custom DI conventions. IServiceCollection.AddDbContext<AppDbContext>() is a known quantity. Your ServiceBus.AutoWire<T>() registration helper that scans assemblies, respects lifetime decorators, and excludes types marked with [ExcludeFromWiring]? The agent is on its own.

The documentation gap isn't just an inconvenience. It is the abstraction's hidden cost, now fully visible.

Skills Files Don't Fix This Either

At this point the counter argument is obvious: just document your abstractions in a skills file or agent instructions file. Drop a CLAUDE.md or .github/copilot-instructions.md in the repo, explain how the custom wiring works, and the agent will have the context it needs.

This is good practice. Do it. But it doesn't solve the problem, it just shifts where the cost lives.

Here's why. The more intricate your abstraction, the more words it takes to explain it accurately. A custom assembly-scanning DI convention doesn't get explained in two sentences. You need to cover what attributes exist, what lifetimes they map to, what assemblies get scanned, what types get excluded, and what order registration happens relative to the rest of startup. By the time you've written that explanation well enough for an agent to act on it correctly, you've written a page of prose that gets loaded into context on every single prompt that touches that part of the codebase.

That is the token tax showing up in a different place. You didn't eliminate it. You moved it from inference cost to context cost.

And that's assuming the skills file is accurate. In practice, these files drift. The abstraction gets updated, the instructions file doesn't. Now the agent has confident, detailed, wrong documentation to work from, which is worse than no documentation at all. Confident wrong guidance is the most dangerous input you can give an agent.

The best instructions file is one that barely needs to explain your codebase because the codebase is already self-evident. Short instructions files are a symptom of a healthy codebase. Long ones are a symptom of accumulated cleverness that somebody eventually had to apologize for in prose.

The Before/After

Here's what this looks like in practice. Two examples, one for attribute-based auto-registration and one for a fluent builder, showing what the agent is actually working with.

Example 1: Registration Convention vs. Explicit Wiring

Before, convention magic:

// Services are "automatically" registered by an assembly scanner
// that reads this attribute at startup. The agent cannot see this
// without tracing through the startup pipeline.
[ScopedService]
public class OrderService : IOrderService
{
    private readonly IOrderRepository _repo;

    public OrderService(IOrderRepository repo)
    {
        _repo = repo;
    }
}

// Somewhere deep in startup...
builder.Services.AddApplicationServices(typeof(OrderService).Assembly);
// ...which calls a custom extension that scans for [ScopedService],
// [TransientService], [SingletonService] and registers them.
// The agent must locate this, read it, and correctly infer lifetime.

After, explicit registration:

// Anyone (human or agent) can read exactly what's registered
// and what lifetime it has. No hunting, no inference.
public class OrderService : IOrderService
{
    private readonly IOrderRepository _repo;

    public OrderService(IOrderRepository repo)
    {
        _repo = repo;
    }
}

// In Program.cs / Startup - no ambiguity, no scanning, no magic:
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();

Yes, the second version is more lines. Those lines cost you nothing to write now, and they save the agent from making a wrong assumption about lifetime that causes a subtle multi-tenant data leak three sprints from now.

Example 2: IQueryable Extension Chains vs. Explicit Filters

Before, chained extension methods masking query behavior:

// Looks clean. Each method name reads like plain English.
// The agent has no idea what any of them actually do.
var orders = await _context.Orders
    .ApplyTenantFilter(tenantId)
    .ApplyUserScope(userId)
    .ActiveOnly()
    .WithRecentActivity()
    .ToListAsync();

// The agent adding a new query doesn't know:
// - Does ApplyTenantFilter add a Where clause or a join?
// - Does ActiveOnly exclude soft-deleted records, or filter by status?
// - What counts as "recent" in WithRecentActivity?
// - Can these be called in any order, or does sequence matter?
// If it omits ApplyTenantFilter, it just wrote a data isolation bug.

After, explicit Where clauses:

// Every filter is visible. The agent can read, reason about,
// and safely modify each condition independently.
var orders = await _context.Orders
    .Where(o => o.TenantId == tenantId)
    .Where(o => o.AssignedUserId == userId)
    .Where(o => o.Status != OrderStatus.Deleted)
    .Where(o => o.LastActivityDate >= DateTime.UtcNow.AddDays(-30))
    .ToListAsync();

The extension chain reads nicely. But when an agent is adding a new query and patterns off existing code, it has to make inferences about what each method does. Miss ApplyTenantFilter and you have a multi-tenant data leak. With explicit Where clauses, the filter is right there in front of it. Explicit filters are self-documenting in a way that named extension methods are not.

Unit Tests Are the Docs That Don't Lie

None of this means you should never build abstractions. It means you should price them honestly. And when you do have custom behavior that must exist, unit tests are the documentation that never goes stale.

When an agent generates code that interacts with an abstraction it doesn't fully understand, tests are the guardrail that catches wrong assumptions before they merge. A test that asserts a query always includes a tenant filter is a contract the agent can be measured against. If it writes a new query method without the filter, the test fails before it merges.

This is doubly true for custom DI wiring and middleware pipelines. A test that exercises the full registration and resolves a scoped service isn't just a regression net; it's a contract that tells the agent exactly what the convention is supposed to produce. If its generated code violates it, the test fails immediately. The feedback loop is tight and the cleanup cost is low.

The combination of boring, explicit code and comprehensive tests is what makes an AI-assisted codebase actually fast. Not clever architecture. Explicit code gives the agent correct assumptions. Tests catch it when those assumptions break.

Rules for the Agent Age

Practical checklist. Apply before you reach for a clever pattern:

  • Prefer explicit registration over convention scanning. Five extra lines in Program.cs costs nothing. A wrong lifetime assumption costs a production incident.
  • If your abstraction can't be explained by pointing at a public doc, reconsider it. IServiceCollection, DbContext, IHttpClientFactory are all known quantities. Your internal ContextualEntityResolver<T> is not.
  • Flatten extension chains into explicit conditions. Custom IQueryable extensions are easy to omit and impossible for an agent to verify without reading their implementation. Explicit Where clauses are harder to get wrong and immediately visible in code review.
  • Write the boring version first. Let the agent suggest the abstraction if a pattern emerges across three or more implementations. Agent-suggested abstractions tend to be grounded in what it can reason about.
  • Document custom abstractions like a public library. XML doc comments, README sections, usage examples. If you wouldn't publish it to NuGet without docs, don't add it to a codebase an agent is co-maintaining.
  • Use tests as behavioral contracts. An agent working against a well-tested codebase makes dramatically fewer wrong assumptions than one working against a clever but untested one.

The best codebase an agent can work in looks like one a junior developer could navigate on their first week: explicit, flat, well-named, and documented. Not because agents are unsophisticated. They're not. It's because explicitness is the trait that scales. It worked for junior devs. It works for senior devs three years after they wrote the code. It works for agents generating features at 3am without you in the loop.

Boring code used to be something you apologized for. In the agent age, it's a competitive advantage.


What patterns have you seen trip up AI agents in your codebase? I'd love to compare notes. Find me on LinkedIn.

Get In Touch

I'm always interested in discussing innovative technology solutions, strategic partnerships, and opportunities to drive digital transformation. Let's connect and explore how we can create value together.

The Hidden Token Tax of Beautiful Abstractions - Steve Ruben