Objects Are Dead, Long Live Feature Slices

Why I stopped chasing reuse and started writing domain logic around decisions, not abstractions.

Objects Are Dead, Long Live Feature Slices
Image Credit: gorodenkoff

For years, we’ve been trained to chase reuse.

Don’t repeat yourself. Extract logic. Share rules. Centralize state. Wrap it all in nice little objects. It sounds smart. Efficient. Clean. But in practice, it leads to the opposite.

You start with one rule. Let's say, a device can’t be bound once it’s retired.
Then someone adds “a retired device can’t be renamed.” Next, someone else adds an edge case: “except when it's marked for re-inspection.” So you extract a shared validator or something like that. Then this helper grows conditions, and you need to test the helper. Then you wonder what breaks when you touch it.

That’s not reuse! That’s coupling.

I used to build code like that. It felt organized, abstract, elegant.
But over time, it got messy. Hard to follow. Full of invisible dependencies.

Today, I see it differently. I don’t want shared rules. I want explicit ones.
I don’t want smart objects. I want dumb data and focused decisions.
I don’t want reuse in my domain logic, not unless it’s earned.

Let’s talk about why.

Why Explicit Rules Beat Shared Logic

Let’s take a simple domain rule: A retired device can’t be bound to an asset.

That rule only matters when you bind a device. Not when you rename it. Not when you inspect it. Just in that one feature.

So why should it live in a shared helper? Why should it become a global isRetired() method that everyone calls without context?

In my model, each feature owns its own rules.
The bind-device-to-asset slice loads the events it needs, reconstructs the relevant state, and checks whether the device was retired.
It does that explicitly, right there, in the decision function. No indirection. No lookup. No magical helper trying to guess the situation.

Now, you might say: “But what if I need the same check in another feature?

That’s fine. Then the other feature will load the events it needs and check retirement in its own way.
It might care about different events. It might apply different exceptions. That’s the point.

Rules look similar but behave differently depending on context.

By scoping the rule to one place, you avoid overgeneralizing. This means you won't end up with a complex helper that tries to serve every case with edge flags, optional parameters, and increasing complexity.

Shared Events vs. Shared Logic

I’m not against sharing. I’m just careful about what we share.

Events are shared. Logic is not.

The event store is the source of truth.

If a DeviceRetired event occurs, it is visible to all. Every feature slice can query it. But how you interpret that event? That’s contextual. That belongs to the feature.

The bind-device-to-asset feature may interpret DeviceRetired as an absolute barrier. However, the rename-device feature may allow renaming even after retirement, unless another event such as DeviceLocked is present.

The same underlying data applies. Different rules. Different decisions.

That’s why I don’t believe in centralizing these checks. A rule is only meaningful in a specific flow. When you abstract it too early, you lose that meaning and invite bugs the moment someone “reuses” it in a place it doesn’t belong.

So yes: Events are shared across slices. Rules are not.

And that separation is what keeps your system flexible, understandable, and easy to evolve.

Why Objects Make It Worse

Objects were supposed to make things clearer. Encapsulate state. Bind data and behavior. Model the real world.

But here is the problem: The real world doesn’t think in objects.

Your domain experts don’t say, “Let’s instantiate a device and call the bindToAsset() method.”

They say, “If the device is retired, it can’t be bound to anything.”

That’s a rule. A decision. A constraint.

It’s not tied to a class. It doesn’t belong to an object. It exists because of the process, not the structure. Objects blur that line.

They collect behavior in one place, even if the rules come from different contexts.
So now your Device object has rename(), retire(), bindToAsset(), maybe even getInspectionHistory().

It does too much. It knows too much. And every time you change one part, you risk breaking another.

It violates Command-Query Separation (CQS).

One method mutates, another retrieves. Same object. Mixed responsibilities. No isolation.

That’s why I say: objects are a bad fit for domain logic. They promote coupling over clarity. They group behaviors not by intent, but by data.
And they make it hard to reason about rules, because everything depends on everything.

Let the data stay dumb. Let rules live in the feature itself. And let behavior emerge from pure functions, not from methods on an object.

Feature Slices Think Differently

A feature slice doesn’t start with an object. It starts with a use case.

Take bind-device-to-asset, for example. That’s not a method of the Device object. It’s a flow. It has an input (device ID and asset ID), a current context (what is the state of the device?), some rules (can it be bound?), and an outcome (success or failure, expressed as an event).

That’s it.

You don’t need to know how the device was created. You don’t care if it has other capabilities.

All you need is what’s relevant for this one thing. And that’s the mindset:

Do one thing. Use only what’s needed. Keep it local.

Feature slices decouple

Feature slices don’t share state. They don’t reuse rules. They build their own view of the truth, using events from the event store.

Yes, sometimes that means repeating the way events are queried and interpreted. That’s totally fine. It’s explicit. It’s visible. It’s traceable.

You know exactly which events a feature cares about. And when a rule changes, you know exactly where to look.

The result?

  • Smaller, simpler units
  • Fewer side effects
  • Better alignment with how the business actually thinks
  • And a system that’s easy to grow because each feature stands on its own

This isn’t just a different code structure. It’s a different way of thinking.

Note: Vertical Slice Architecture, with its built-in separation of commands and queries, is fundamentally orthogonal to layered architectures and object-oriented design. It structures code by intent and outcome, not by technical role or class hierarchy.

Conclusion

For a long time, I thought good software meant reusable software.
I tried to model the domain with rich objects, centralize shared logic, and keep my code DRY.

But over time, that created more problems than it solved.
Small changes had wide effects. Simple rules turned into abstractions. And every reuse brought hidden assumptions I couldn’t see.

The turning point was when I stopped chasing reuse and started chasing clarity.
I began treating each feature as a small, self-contained unit: its own state, its own rules, its own decisions. The only thing shared across slices are pure events.

That shift changed everything.

Now, rules are visible. Decisions are local. The code reads like the business talks.
And when something changes, I know exactly where to go.

So no, I don’t want global states, aggregates, or smart entities anymore.

I want rules in context, behavior in slices, and code that earns its simplicity not through abstraction, but through explicitness.

Cheers!

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