Pakkit.net
← Back to blog

Systems Thinking

A Plugin Is a Contract and a Blast Radius

Plugin architectures buy you modularity, but each plugin is also a trust boundary and a failure boundary — and the success, failure, and error paths around every invocation are the part people forget to design.

  • Systems Thinking
  • Architecture
  • Reliability
  • Design

I’ve spent a lot of time inside a system built almost entirely out of plugins — small modular units that each do exactly one job: read a file, query a database, run an authentication method, write a log. You compose them into a flow, and each step hands off to the next. It’s a genuinely good architecture. It also taught me that a plugin is never just a reusable component. Every plugin is two things at once: a contract about what it promises, and a blast boundary for what happens when that promise isn’t kept.

Modularity is the easy half

The appeal of a plugin model is obvious and real. Each unit does one thing, so it’s easy to understand, test, and swap. The flow that composes them reads like a description of the work: do this, then this, then this. New capability means a new plugin, not surgery on a monolith. This is the half everyone designs for, because it’s the half that shows up in the happy-path demo.

The trouble is that the happy path is where every invocation succeeds. In a flow of ten plugins, you’re not really designing ten steps — you’re designing ten boundaries, and a boundary is defined by what crosses it in both directions, including the bad directions. The modularity is the part that sells the architecture; the boundaries are the part that determines whether it survives contact with reality.

Every invocation has three outcomes, not one

The thing the good plugin systems force you to confront: invoking a plugin doesn’t have one result, it has at least three. Success, failure, and error are different, and they need different handling. In the engine I worked in, you literally couldn’t call a plugin without saying what happens on success, what happens on failure, and what happens on a hard error — the syntax demanded all three branches.

“It worked” is one of the answers an invocation can give you. “It said no” and “it broke” are the other two, and they are not the same answer.

That requirement felt heavy until I’d written enough flows to see what it prevented. A database lookup that fails (no match) is a normal business outcome; a database lookup that errors (connection refused) is an operational fault. Conflating them is how a transient outage gets treated as “user not found” and a missing record gets treated as a system crash. Being forced to name all three paths means you can never accidentally pretend an invocation only has one.

The contract is the inputs and every output

So the real contract of a plugin isn’t just “what does it do.” It’s the full surface: what inputs it requires, what it produces on success, what it means when it fails, and what it does when it errors. A plugin you can only describe in terms of its happy-path output is a plugin you haven’t finished specifying. The failure and error semantics are part of the interface, not an afterthought you discover in production.

This is the same idea as treating error codes as an API contract: the ways a component can say “no” or “I broke” are as much a part of its public interface as the way it says “yes.” A caller that only handles success isn’t using the component, it’s gambling on it.

A plugin is a blast boundary too

The other half of “blast radius” is containment. Because each plugin is a discrete unit with a defined boundary, it’s also the natural place to contain a failure — but only if you actually use the boundary that way. A plugin that does one job can fail at that one job without taking the whole flow down, if the flow handles its failure path deliberately. The boundary is an opportunity; it doesn’t enforce itself.

This is where the one-job discipline pays a second dividend. A plugin scoped to a single responsibility has a small, comprehensible failure surface — you can reason about exactly what breaks when it breaks, and what the flow should do about it. A plugin that quietly does three things has a blast radius three times as hard to reason about, and a failure in it is three times as ambiguous. Narrow scope isn’t just clean design; it’s what keeps the failure boundary legible.

Designing extensible systems with this in mind

When I design or evaluate anything plugin-shaped now — and that includes the way I wire up tools for AI agents — I hold every extension point to the same standard:

  • One job per unit. A plugin that does one thing has a failure mode you can name.
  • Specify all three outcomes. Success, failure, and error are distinct; the interface isn’t complete until each is defined.
  • Make the failure path explicit at the call site. “What happens when this says no, and when this breaks?” is a design question, answered up front, not a default you inherit.
  • Treat the boundary as containment. The whole reason to have discrete units is so a failure in one doesn’t become a failure in all — but you have to route the failure for that to be true.

The same thinking shows up when I build an agent’s tools like real services: a tool, like a plugin, is a contract plus a blast radius, and the failure semantics are where the real design lives. A plugin model gives you clean modularity for free and makes you earn the reliability. If you’re designing an extensible system and want to talk through the boundary semantics, I’m easy to reach.