DEV Community

DimiDan
DimiDan

Posted on

Feels like a feature until it's infrastructure

A while ago I wrote about pricing logic and why it deserves to be treated as infrastructure instead of a pile of UI code. This is the same lesson from a different subsystem: undo/redo in a real-time, event-driven editor. The difference is that this time the payoff is visible, and it showed up a year later in a place we weren't looking.

Features and invariants are different objects

Features have a definition of done you can see. Ship the button, click the button, close the ticket. When a feature half-works, someone notices, because the feature is the thing people look at.

Infrastructure is different. Its job is to hold invariants that other code relies on. For undo/redo the invariant is brutal: every state mutation, from every code path, present and future, must be reversible in chronological order. That invariant doesn't live in any one component. You can't screenshot it. When it erodes, it erodes silently, one unmigrated code path at a time.

Pricing had the same shape. The invariant there was "money math is exact and happens in exactly one place." Every shortcut that looked harmless locally — a parseFloat here, a float multiplication there — chipped at an invariant nobody owned. It was boring until it was wrong.

The thing that decides whether you get this right is a question asked early: is this a feature, or is it infrastructure wearing a feature costume?

The tell: closures vs commands

You can often tell which mental model produced a piece of code by looking at what the abstraction knows about itself.

The obvious way to build undo support is callbacks. Every widget that wants to participate registers a pair of closures: here's how to undo me, here's how to redo me. It works, and it's the version most editors start with.

The problem is that a callback-based undo entry knows nothing. It's two anonymous closures. You can't ask it what it does, which block it touches, whether it succeeded, or when it happened. You can only fire it and hope. Debugging means reading captured variables in a debugger. Testing means reconstructing the exact closure environment.

The command version is self-describing:

class DeleteBlockCommand {
  readonly scope = 'block';
  readonly timestamp: string;
  getCurrentBlockId(): string;
  async undo(): Promise<boolean>;
  async redo(): Promise<boolean>;
}
Enter fullscreen mode Exit fullscreen mode

That difference looks cosmetic. It is not. Because commands carry identity, the system built on them can do things the closure version never could:

  • One chronological stack across text edits, block moves, and table changes, because everything is comparable.
  • Retry and failure semantics, because undo() returns a result instead of throwing into the void.
  • Batching, because commands compose.
  • Analytics at a handful of seams instead of everywhere.

When architecture pays off

That last bullet deserves its own section, because it's where the decision paid for itself.

When we wrote the RFC for the command architecture, one of its stated purposes was to reify editor actions as discrete, self-describing objects. Text edit, block move, style change, table mutation — each one individually addressable, with its own scope, timestamp, payload, and success or failure resolution, all landing on one chronological stack. Granular, per-event control over what happens in the editor. The justification at the time was correctness: reliable undo, reliable ordering, reliable failure handling.

Then product came asking for interaction tracking. Which actions do users perform, how often, in what order.

Look at what an analytics event needs: a name, a subject, a timestamp, structured properties, and confirmation that the thing actually happened. That is a command object. The grain analytics needed was the grain the architecture already had.

Without the command layer, per-action tracking means scattering emit calls across dozens of UI components and hoping every future component remembers to do the same. It also means every one of those call sites independently deciding what counts as success, which in practice means some of them fire on intent rather than outcome and your funnel quietly overcounts.

With the command layer, tracking attaches at a handful of well-defined seams and inherits success-gating for free. No event fires for an action that failed or got rejected, because the command already had to know the difference in order to be undoable.

We didn't design the command pattern for Mixpanel. We designed it for control over events, and analytics turned out to be just another consumer of that control. That's the signature of an infrastructure decision: consumers it wasn't designed for show up later and find what they need already there.

How to spot infrastructure wearing a feature costume

Four questions that would have flagged both pricing and undo/redo early:

  1. Does it cut across features? If every new widget has to remember to participate, it's infrastructure, and "remember to" is not a mechanism.
  2. Does it fail silently? Features fail loudly, in someone's face. Invariants fail quietly, in aggregate, and the first report is a metric that looks slightly off.
  3. Is correctness invisible? If you can't tell it's broken by looking at the screen, you need the architecture to make wrongness impossible, not code review to catch it.
  4. Is "mostly done" meaningfully different from done? For a feature, eighty percent shipped is roughly eighty percent of the value. For an invariant it's zero: a single unmigrated code path is enough to make the guarantee untrue, and untrue guarantees are worse than absent ones because everything downstream is built on them.

The uncomfortable part is that infrastructure decisions are hard to justify during planning, because the strongest argument for one is a consumer you can't name yet. You're asking for extra work now against a payoff you can only describe in the abstract.

A year later, ours had a name.

Top comments (3)

Collapse
 
david_crystal_67d49f98f74 profile image
David Crystal

This is a truly insightful write up about an often underestimated topic.

I agree with everything presented, but the main point that stands out to me is the nuance between what should be a feature and what should be an invariant. The example about how undo/redo is a feature that "almost" works but is fundamentally broken at its core is a great one. Another thing I really liked was the discussion around closure vs command and why command is the right approach.

The ability to use commands as self documenting units has huge benefits (as described in the debugging section) beyond just being able to undo or redo. The fact that closures could be reused in different contexts was also a great point. The other thing I really liked was that good infrastructure cannot be properly justified in cost benefit analyses and is often underprioritized as a result. Your point about analytics being a huge benefit as a result of commands being self documenting was a great example of this.

Overall this is a fantastic post about an extremely important topic. I think your post really captures why infrastructure is such a huge benefit and why it should be prioritized more often than it is.

Collapse
 
dimidan profile image
DimiDan

Thank you, really appreciate you taking the time to read it so closely.

Yes, the main point is developing the ability to recognize the long-term impact of our immediate decisions. And that's the hard part, because most of the time when we design new architecture or infrastructure we're working under a time constraint, with the constant temptation to over-engineer pulling in the opposite direction. Finding the line between the two is most of the job.

Collapse
 
david_crystal_67d49f98f74 profile image
David Crystal

Absolutely. I think that balance, that’s when it gets technical. You have to focus on the thing for which solution you’re looking for. You have to focus on a particular aspect. And the other aspect is how those decisions that you make now will affect the system’s operation in a few months or years. The art of making the decision is when to do it simply and when to invest in the future.