The Abstraction That Hid the Only Logic That Mattered
Duplessis van Aswegen

Duplessis van Aswegen @duplessisvanaswegen

About: Software engineer | Web dev enthusiast | Side project tinkerer | Music & gaming fan | Always learning & building. Let’s connect!

Joined:
Apr 13, 2024

The Abstraction That Hid the Only Logic That Mattered

Publish Date: Jun 2
3 3

It’s Always the Refund

It started, as these things always do, with a Slack message from support that began:

"This is probably nothing, but…"

It never is.

Turns out a customer had requested a refund for a $0 order—paid entirely with a promotional code that someone in marketing named “YOLO100.” Our system, ever obedient, generated a pristine digital refund for exactly zero currency units and confidently shoved it into the payment gateway, which replied with the transactional equivalent of a raised eyebrow:

"Invalid amount."

From there, the trail of confusion expanded outward like a suburban sprawl. Engineering said it shouldn’t be possible. QA said it wasn’t in the test cases. Product said it was “an edge case.” (They always say that, don’t they? Every single cliff we fall off was once called an edge case.)

We eventually traced it back. The logic that was supposed to prevent this refund—"don’t send zero dollars to Stripe, you absolute gremlin"—existed. Technically.

It was just hidden inside a validator that only ran in one flow, skipped in another, and silently assumed the refund had already passed through a gauntlet of sanity checks. Spoiler: it had not.

Turns out, we had abstracted away the only logic that actually mattered.


Architecture by the Book (That No One Reads)

On paper, it was gorgeous.

A shining example of Clean Architecture, blessed by the gods of SOLID and curated with the gentle hands of someone who’d read the book twice. Everything had its place. Domain logic in the core. Application services on the edge. Infrastructure politely waiting outside with its hat off.

It was the kind of diagram you put in a pitch deck to make non-technical stakeholders nod solemnly.

At the heart of it all: Business Rules. Separated. Encapsulated. Respectable. We put them into validators and policies, wrapped those in handlers, and made sure they were only invoked by orchestrators with very strong boundaries and very weak opinions.

Command buses dispatched commands. Decorators added behavior. Rules were extracted, reused, blessed with unit tests, and wrapped in so many layers of pattern that they might as well have been a wedding cake.

The goal was purity. Composability. Decoupling.

You know. Architecture stuff.

What could go wrong?

Spoiler: everything.

The actual refund logic, if you dared to follow it, looked less like a business workflow and more like the migratory pattern of a particularly indecisive pigeon.

The request hit the controller, which fired off a command. That command passed through three decorators, a logging bus, a metrics wrapper, and then landed in a handler that did… very little. It mostly just handed the logic off to a service, which fetched a RefundPolicyManagerFactory and asked it, very politely, what to do next.

If the stars aligned, and the correct refund context was passed via middleware, and the policy registry hadn’t been short-circuited by a feature flag (true story), it would eventually reach the logic that said: “Oh, by the way, maybe don’t refund zero dollars.”

But in our case, the order had been placed using a full-discount coupon. That triggered a “discounted refund path,” which skipped the decorator chain entirely. Because, of course, we had more than one refund flow. We had three. One for standard refunds, one for coupons, and one for auto-cancellation—which didn't run validation at all because we assumed the system could do no wrong.

Each one was almost right.

But none of them carried the only rule that mattered.

The logic was everywhere and nowhere. A Schrödinger's condition: present in the system, but not observable.

Support couldn’t explain it. QA couldn’t catch it. PMs didn’t know it existed. And developers? We were left spelunking through a cave system of indirection, carrying only a headlamp and our IDE’s “Find All References” feature.

I once diagrammed the refund logic for onboarding. Halfway through, I switched to using emojis.


When Abstraction Becomes Camouflage

Let’s call it Abstraction Drift, though I was tempted to go with Layer Cake of Denial.

You start with a noble idea: separate your concerns. Pull out your logic. Wrap it in a reusable rule. Inject that rule via interface, wrap the interface in a service, and protect the service with a decorator. Toss in some middleware for good measure. Abstract until pure.

And then, one day, a critical rule—the thing that stops you from issuing Monopoly money refunds—lives four files away from the Refund itself, guarded by an interface nobody reads, and quietly skipped because the wrong flag was set.

This isn’t just over-engineering. It’s logic laundering.

By the time a bug appears, the rule responsible for preventing it is so thoroughly abstracted that you need a Ouija board to find it.

The abstraction didn’t isolate the logic. It dissolved it.

What was meant to be composability became composure theatre—a performance of order, with no actual predictability.


Seeing Is Debugging

Here’s what I learned, after months of architectural self-loathing:

If your system’s correctness depends on rules the developer can’t see, you didn’t abstract—you disappeared.

An abstraction should make things easier to reason about. If it doesn’t, it’s not an abstraction. It’s a trapdoor.

The problem isn’t just indirection. It’s invisible indirection. It’s the validator that only sometimes runs, the rule that lives in a helper, the condition buried behind a feature flag named betaRefundLogicV3.

It’s abstraction with amnesia—logic stripped of context, responsibility, and visibility.

So now, when I design something, I ask a very specific question:

Can I see the important behavior where it happens?

Or do I have to pray that some upstream service did the right thing?

And if the answer is the latter, I pull the logic back. I put it next to the thing it affects. I let the Refund know about the rule. Not because it’s “pure,” but because it’s real.

Because if your abstraction hides the logic that makes the system safe, it’s not clean. It’s complicit.


The Refund Strikes Back

We fixed the refund bug.

Eventually.

The logic now lives in the Refund, right where it always pretended to be.

We still have validators. But they decorate nothing.

Kind of like me at our last architecture review.

Balance has been restored to the Force.

Or at least to refunds. Which, in this system, are basically the same thing.

Comments 3 total

  • david duymelinck
    david duymelinckJun 2, 2025

    I'm very wary about feature flags. For me a feature flag is always temporally. If it is a long term solution it needs to be a setting.

    We were left spelunking through a cave system of indirection, carrying only a headlamp and our IDE’s “Find All References” feature.

    There is no tracking feature in the command bus? That would make it easier to find out the correct flow is used.

    • Duplessis van Aswegen
      Duplessis van AswegenJun 6, 2025

      Absolutely agree - feature flags are like houseguests: lovely when they visit briefly, but if they start forwarding their mail, you’ve got a problem! A flag that sticks around too long is basically just a config setting that never got therapy.

      As for the spelunking - There was some tracking in the command bus, but it mostly told us where the train went, not why it derailed. The command itself was generic enough to be reusable (yay), but that also meant it silently shape-shifted based on flags buried two layers deep. So when something weird happened, you couldn’t just follow the trail - you had to interpret the shadows on the wall, Plato-style.

      Honestly, I think a clearer split - even just separate commands for different business intents - would’ve saved us hours of IDE archaeology. But hindsight, as always, is 20/20 and slightly smug.

      Curious how you approach this in your setups - do you have flag linting? Feature flag expiry rituals? Or just better discipline and fewer regrets?

      • david duymelinck
        david duymelinckJun 6, 2025

        Feature flags are a relative new thing for me, so i had the time to analyse their benefits while still using settings. So settings are my default, but with feature flags you can say they are temporary so you can set a evaluation time where the decision is removal or changing it to a setting.
        You can do that with a setting too, but with a new concept like a feature flag it becomes more explicit.

        This would not stop the problem you had. I think because people see feature flags as something temporary they don't do the full analysis of the consequences. I have fallen in that trap to.

Add comment