When Reuse Breaks Decoupling

Why self-contained feature slices collapse the moment we start orchestrating them

When Reuse Breaks Decoupling
Image Credits: Dekdoyjaidee

There is a very specific moment in almost every system where things start to feel slightly off. Nothing is broken. Tests are green. The architecture still looks clean. But you notice that implementing a new feature suddenly requires touching two or three existing ones. Not because the behavior truly changed, but because the structure demands it.

That moment is subtle. It rarely comes with a dramatic failure. It usually comes with a small improvement, a tiny refactoring, or a bit of reuse.

This article is about seemingly harmless features, such as rotating an API key. And how the instinct to reuse existing features quietly undermined everything that self-contained feature slices, Functional Core/ Imperative Shell, and event sourcing are supposed to give us.

More importantly, it’s about how easy it is to fall into the reuse trap, even when you know better.

The innocent idea: rotate means create plus revoke

Rotating an API key sounds trivial. You create a new key. You revoke the old one. End of story.

If you already have a create_key feature and a revoke_key feature, the natural thought follows immediately: Why not just orchestrate them? Call create_key, then call revoke_key, wire them together, and you’re done. No duplication. Clean reuse. DRY. Responsible engineering.

That instinct is deeply ingrained in most of us. We’ve been trained for years to see reuse as a virtue and duplication as a failure. So the move feels almost automatic. And this is exactly where things start to go wrong.

The first smell: orchestration that “just coordinates”

At first, the orchestration looks innocent. A rotate_key feature that does little more than glue two existing operations together. It doesn’t make decisions, it doesn’t add rules, it just coordinates. Or so it seems.

But then you need to check whether the old key exists. So you reach into the revoke feature to load some context. Then you need to pass scopes, kinds, roles, client IDs. So you start sharing command types or helper functions.

Then you want consistent error handling. So you import rejection types. Before you notice it, rotate_key imports half of create_key and half of revoke_key. And suddenly changes in those features ripple into rotate_key. Tests that should be independent start failing together. Refactoring one feature requires understanding another. At that moment, something fundamental shifted.

The real mistake: treating actions as reusable units

The core mistake here is subtle, but decisive.

We treated actions as reusable units. Create a key. Revoke a key. Those look like generic actions, so we assume they can be composed freely. But in a system built with a self-contained feature slice mindset., features don’t exist to perform actions. They exist to own invariants.

That distinction matters more than any abstraction or pattern.

Invariants define features, not steps

Let’s look at what the features actually mean.

create_key owns an invariant. Something like: a key can be created if plan limits allow it, scopes are valid, and the tenant state permits it. It decides whether creation is allowed and records the fact if it is.

revoke_key owns a different invariant: a key can be revoked idempotently, safely, and permanently.

Now look at rotate_key. Its invariant is not “create and revoke”. Its invariant is: after rotation, there is exactly one active key replacing the previous one. That’s a different rule. A different guarantee. A different meaning.

Once you see that, it becomes obvious that rotate_key is not an orchestration of two features. It is a feature in its own right.

Trying to express it as reuse was the mistake.

Orchestration is where meaning starts to leak

The moment a feature starts orchestrating other features, it has to understand their meaning. It has to know what “exists” means for revoke. What “created” means for create. What failures matter and which ones don’t.

That means orchestration is not neutral. It is semantic glue, and semantic glue is just another domain layer, whether you call it that or not.

This is exactly how shared domain layer or service sneaks back into systems that were supposed to avoid it. Not through big frameworks or repositories, but through “helpful” orchestration layers that know a little too much.

Feature APIs and why they still didn’t feel right

One possible reaction is to introduce a feature registry or API. Let features expose executable commands, and let orchestrators call those without importing internals.

This might sound like a clean solution: no direct imports, clear boundaries, and explicit wiring. But if you’ve ever gone down that path, you know the feeling: something still isn’t right.

You end up with centralized command definitions. Shared result types. A growing registry that slowly turns into a service locator. And most importantly, you’re still orchestrating semantics across features.

You avoided technical coupling, but you didn’t avoid conceptualcoupling.

That’s why the discomfort remained.

The moment of clarity: stop orchestrating, start owning

The real fix wasn’t to improve the orchestration. It was to remove it. rotate_key should not coordinate create_key and revoke_key. It should own its own decision. That means it loads its own context. It checks plan constraints itself. It decides how a new key replaces an old one. It appends its own events.

Yes, some mechanics are duplicated. And yes, some checks look similar. And that’s fine. Because duplication of mechanics is cheap. Coupling of meaning is expensive.

Why duplication is not your enemy

We need to be honest about what duplication actually costs.

Duplicating a few lines of validation logic costs almost nothing. Duplicating context loading costs a bit of code. Duplicating decision rules that are conceptually different costs clarity, not correctness.

But coupling features through orchestration costs you:

  • local reasoning
  • independent refactoring
  • safe evolution
  • test isolation

In long-lived systems, that cost compounds fast.

Event sourcing without shared entities makes this even clearer

In an aggregateless event-sourced system, features append facts. They don’t mutate shared objects. They don’t coordinate through in-memory state.

That means the natural unit of decomposition is the decision that emits facts, not the sequence of steps that happen to look similar.

When rotate_key appends events that represent “old key replaced by new key”, it doesn’t matter that create_key and revoke_key append similar facts. The meaning is different. The causality is different.

Trying to reuse those features was reversing causality: structure first, behavior second.

Audit logging reveals the same trap from the other side

The same thinking applies inreverse with something like audit logging.

If an audit feature imports create_key, suspend_key, or rate_limit, it’s already wrong. Audit doesn’t do anything. It explains what happened.

Audit should project facts, not recompute logic. The moment it imports feature logic, it stops being a projection. Again, the fix is not better reuse. It’s stricter ownership of meaning.

Why this matters beyond one feature

After stepping into the reuse trap and stepping back out, one rule became painfully clear:

If a workflow has its own invariant, it deserves its own feature slice. It’s not an orchestrator, coordinator, or glue layer but it’s a feature in its own right. Once you adopt that rule, a lot of architectural decisions suddenly become easy.

This isn’t really about rotating API keys. It’s about resisting the constant pressure to centralize meaning.

Every time you feel the urge to “just reuse” another feature, ask yourself:

  • Am Ireusing mechanics, or am I sharing meaning?
  • Does this workflow have an invariant of its own?
  • Wouldremoving this feature later require touching others?

If the answer to the last question is yes, you’re already coupled.

Feature slicing don’t fail because people don’t know the patterns. They fail because people stop trusting duplication and start trusting structure.

  1. They fail when we optimize for elegance instead of independence.
  2. They fail when we treat reuse as a goal instead of a consequence.

Closing thought

I briefly fell into the reuse trap myself. That’s an important part of this story. Not because it’s embarrassing, but because it’s honest.

If you’re building systems that are meant to evolve for years, you will constantly be tempted to orchestrate instead of owning. To abstract instead of deciding. To reuse instead of duplicating.

The discipline is not in knowing the right patterns.

The discipline is in knowing when not to apply them.

And sometimes, the most decoupled architecture is the one that dares to repeat itself.

Cheers!

This article was published originally here.

Subscribe to Rico Fritzsche

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe