DEV Community

Kamen
Kamen

Posted on Originally published at kamenivanov.substack.com

Why almost every ‘Temporary’ Workaround is Permanent (Chapter 8)

Somewhere around month four of any project I've worked on, someone says a version of the same sentence. "Just hardcode it for now, we'll fix it after the release." Nobody arguing against it and they genuinely believe it's temporary. The problem isn't the shortcut itself, sometimes a shortcut is the correct call. The problem is that "temporary" has no owner, no ticket, and no deadline of its own. It just sits there until someone forgets it was ever supposed to be temporary at all, and by then it's load-bearing. Here's how that actually plays out, because the abstract version of this story is boring and everyone nods along without changing anything.


The shortcut

We needed to ingest pricing updates from an upstream provider. Three days, full stop, because a partner integration deadline had already been communicated externally and nobody wanted to be the one calling to push it back. The fastest path was obvious: inject a RestClient call directly into the business-logic service method that needed the data, parse the JSON inline, map a couple of fields by hand, and move on.

We shipped it on time, it worked and everyone moved on to the next fire, which is what always happens after a deadline shortcut lands successfully. Success is exactly what makes it invisible.

Two months later we get an update: "The upstream team is moving to Kafka. They want to push events, not serve a REST endpoint anymore.", and now the HTTP call is gone, but it was never just a call - it was call semantics baked into a domain-facing method signature. The service method that used to return synchronously now needs to react to a stream. Unit tests that mocked an HttpClient are gone and now they need to mock a Kafka consumer instead. Every call site that invoked the pricing method synchronously has to be reconsidered, because the whole shape of "when do we have this data" changed underneath them.

Three weeks after that migration finished, we got again another update: "Edge devices in the field can't maintain a Kafka connection reliably. We're pushing over MQTT instead." Same story again, except now it's the third rewrite of the same core logic in five months, and each rewrite touches more of the codebase than the last one did, because more things had grown to depend on the leaked transport assumptions in the meantime.

Nobody planned for three transport migrations. Nobody ever does. That's exactly the point - the shortcut wasn't wrong because REST was the wrong choice, it was wrong because it welded a decision that was going to change into logic that was supposed to be stable.


What it should have looked like

This is precisely why business-logic in this project never talks to a transport mechanism directly. It depends on an interface, a port, if you want the textbook word for it, though I'd rather just call it what it is: an interface that answers the domain's question ("give me the latest price for this SKU") without knowing or caring how the answer arrives. Whether that interface is backed by a REST client, a Kafka listener, or an MQTT subscriber is bridge-impl's problem, sealed away from business-logic by the same compiler-enforced module boundary that's been the whole point of this architecture since Chapter 1.

When the transport changes, bridge-impl changes. The interface in bridge-api doesn't move, and neither does a single line in business-logic. Three transport migrations in five months would have been three rewrites of one implementation of the interface instead of three rewrites of everything downstream of a leaked implementation detail.

The uncomfortable part of this story is that we didn't have that boundary in place when the first shortcut happened. We built it because of this exact pain, not before it. That's usually how it goes, the boundary gets built in the shape of whatever already went wrong once, the same way most safety rules only exist because a crash already happened.


The same failure, dressed differently

The transport story is the clean, satisfying version, because it has a name and a fix. The uglier version of the same failure shows up whenever someone decides a mapper is overhead the deadline can't afford. "Why write a DTO and a transformer? Just return the entity from the controller." It takes five minutes to wire up and it works immediately, which is exactly the illusion - the cost isn't paid at the point of the shortcut, it's paid the first time someone needs to rename a column and discovers that half a dozen frontend clients have quietly come to depend on the JPA entity's field names as if they were a public contract.

This is what the Transformer/BiTransformer split from Chapter 4 actually buys you, beyond the argument against MapStruct. It's not really about avoiding a mapping library, It's about there being exactly one place where "how the database shape becomes the domain shape" is decided, so that decision can change without becoming an incident. Skip it under deadline pressure and you haven't saved the mapping work, you've just deferred it to whoever eventually has to reverse-engineer which fields are safe to touch.


The actual cost, after the fact

We went back and counted, roughly, what the three transport rewrites cost against what the port/adapter boundary would have cost to build up front. Building the boundary correctly the first time would have added maybe a half day or a day to the original three-day deadline - one interface, one adapter implementation, tests against the interface instead of the HTTP client. The three subsequent rewrites, combined, cost closer to three weeks, most of it not writing new code but tracing which parts of the business logic had quietly grown to assume synchronous, request-shaped delivery of data that was never going to stay synchronous or request-shaped.

That ratio is the entire chapter. The shortcut doesn't remove the cost, it just moves it downstream, converts it from a schedule risk into a production risk, and hands the bill to whoever's on call when a customer reports that a price didn't update and nobody can immediately say which of three transport layers is currently responsible for getting it there.


The part that doesn't fit in a retro

The hard part isn't spotting this pattern in hindsight, it's obvious once it's a war story. The hard part is that at the moment the shortcut gets proposed, it is correctly, the fastest way to hit the date. Refusing every shortcut on principle is its own failure mode. You'll miss real deadlines defending abstractions nobody needed. The distinction that actually matters is whether the shortcut cuts a corner inside a module boundary you're allowed to redo later in isolation, or whether it cuts through a boundary that other code is going to grow around before anyone circles back to it. A hardcoded value inside bridge-impl is an annoyance, a hardcoded assumption smuggled into business-logic about how data arrives is a liability with compound interest, and the interest rate is set by how many other developers touch that code before someone pays it down.


What's Next

Chapter 9 continues into the business logic module itself - what it means to write core services that depend on nothing but interfaces they own, and what actually breaks when that discipline slips.

Codebase & Architecture Blueprint

The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 8. There's no code change between Chapter 7 and Chapter 8, I'm tagging each chapter regardless, for consistency:

Maven 3.9.x and Java 25 are required.

◀️ Read Chapter 7: The DAO Testing
▶️ Read Chapter 9: The Core Business Logic Module (Coming soon)

Join the Masterclass Journey

This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, subscribe to my Substack Newsletter here!


Recommended Reading & Interview Preparation

If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot & Architecture skills, check out these highly-rated resources from the Javarevisited publication (Use promo code friends20 for an exclusive 20% discount automatically applied at checkout):

Top comments (0)