$RodHat_
Rod's Tales

The microservice that should have been a function call, and the eighteen months it took anyone to say so

Published by

a close-up of a server room
Photo: Kier in Sight Archives / Unsplash

There was a service, at a company I won’t name because the point isn’t to embarrass anyone in particular, whose entire job was validating a discount code against a set of business rules. Not processing payments. Not touching inventory. Reading a code, checking it against maybe a dozen conditions — expiration, minimum cart value, category eligibility, stacking rules — and returning “valid” or “invalid, here’s why.”

This lived in its own microservice. Its own repo. Its own deploy pipeline. Its own on-call rotation, nominally, though in practice whoever was on-call for the checkout team just absorbed its pages because nobody had separately staffed a “discount validation team,” because of course they hadn’t, because it’s a function.

How it got there

Nobody sat down and decided “we should distribute this unnecessarily.” It got there the way most bad architecture gets there: incrementally, with each individual step looking reasonable in isolation.

The logic started as a method on the checkout service. Fine. Then a second team needed the same validation for a different flow — a customer support tool for manually applying discounts — and rather than share a library, someone extracted it into a service so both teams could call it over HTTP instead of managing a shared dependency’s version bumps across two codebases. This was, I want to stress, not an insane decision at the time. Shared libraries have real coordination costs. Extracting a service has real coordination costs too. Somebody weighed them, on a Tuesday, under deadline pressure, and picked the service.

Once it was a service, it accreted service-shaped problems it never needed: a health-check endpoint, a deploy pipeline, its own database connection pool for a rules table that changed maybe twice a month, retry logic in every caller because now a network hop could fail where a function call couldn’t, and eventually — this is the part that still gets me — a circuit breaker, because the service had gotten slow enough under load that callers needed protecting from it. The load, for a system whose entire job was checking a code against twelve conditions.

The part where nobody could kill it

Eighteen months. That’s how long this thing lived in production, fully understood by roughly everyone who touched it to be absurd, before anyone actually rolled it back into the checkout service where it belonged.

Not because it was hard, technically. It genuinely wasn’t — folding it back in was maybe a week of work including tests. It stayed alive because killing a service, even an obviously overbuilt one, requires someone with the standing to say “we’re removing infrastructure” out loud in a room, and own the risk if removing it breaks something nobody remembered was depending on it. Adding complexity never requires that kind of standing. Nobody’s ever been dragged into a postmortem for extracting a microservice too eagerly. People absolutely get dragged into postmortems for removing one that turned out to matter. The incentive gradient only points one direction, and it’s not toward simplicity.

The actual lesson, and it’s not “microservices bad”

I am not the “monolith or death” guy. Distributed systems solve real problems — independent scaling, independent deploy cadence, fault isolation between genuinely separate concerns. This wasn’t a microservices problem. It was a boundary-drawing problem, and microservices architecture just gave the bad boundary a much more expensive way to be wrong.

The question that would’ve prevented all eighteen months of this, asked at the moment of extraction: does this thing need to fail independently of its caller, or does it just need to be reusable? Those are different problems with different solutions. “Reusable” wants a well-versioned shared library. “Independently failable” wants a service. This code needed the first thing and got handed the second, and the gap between those two answers is where an entire on-call rotation’s worth of unnecessary pages lived for a year and a half.

Draw your service boundaries around actual failure domains, not around “two teams want to use the same logic.” The second problem has a cheaper solution than the first one, and conflating them is how you end up with a circuit breaker protecting a discount code check.

Design Patterns gets dunked on for being dated, but the actual lesson — name the problem before you reach for a structural solution — is the same lesson this whole post is about, just with worse marketing. The Art of Unix Programming is the other half: do one thing, compose instead of extract.