The client project that quietly became a product
I knew I'd built a product the day I edited the same notification service twice in one sitting, in two different backends, fixing the identical bug in both.
I knew I’d built a product the day I found myself editing the same notification service twice in one sitting. Same logic, same bug, two completely separate backends, because somewhere along the way the estate had drifted into maintaining identical code in two places and nobody had decided that out loud.
Nobody hands you a moment where a contract job becomes a company. There’s no kickoff, no rename, no celebratory commit. You build a bespoke system for one client, and somewhere around the third feature you look up and realize the thing you’ve been treating as a one-off has quietly grown a spine, a shape, a set of opinions that would work just as well for the next person who walked in with the same problem. The realization is humbling precisely because it’s so undramatic. You didn’t decide to build a product. You just kept solving the obvious next problem until one existed.
Duplication is the tax that tells you
The two-backends moment wasn’t a near miss. It was the diagnosis. When you catch yourself making the exact same change in two places to keep two installs behaving the same way, the duplication isn’t the problem, it’s the symptom. The architecture is telling you it has outgrown the assumption it was built on, which was “this is one system for one client.”
- Duplication is a tax you pay forever. Every future fix to that notification logic now costs double, and the day you forget the second copy is the day the two installs silently disagree about something a customer can see.
- It compounds in the dark. The drift doesn’t announce itself. It accumulates quietly across releases until a bug exists in one backend and is already fixed in the other, and you can’t remember which is the truth.
- It’s a signal, not a chore. The fix isn’t “be more disciplined about copy-pasting.” The fix is to admit the two installs are the same product wearing two coats, and to build the one engine they should both have been running all along.
The hard part isn’t the code
Pulling shared logic into one engine is, mechanically, a refactor. I’ve done harder ones before lunch. The genuinely hard part is the line you have to draw down the middle of the thing: what is the engine, and what is just this client’s config.
That boundary is the whole product. Get it right and a new customer is a row of settings and a fresh database. Get it wrong and you’ve built something that’s technically reusable but in practice has one client’s assumptions welded into the core, so the second customer needs a fork, and now you’re back to two backends with extra steps. Every place where a client name, a hard-coded rule, or a “they always do it this way” leaks into the engine is a place the product will tear when you stretch it over a second account.
A product isn’t the code you can run for two clients. It’s the line you’ve drawn between the part that’s the same for everyone and the part that’s allowed to differ, written down on purpose.
The discipline of not starting it yet
Here’s the part nobody warns you about: realizing you’ve built a product is dangerous, because the temptation is to immediately go build the product. Stop the client work, rip everything apart, do the grand extraction now, while the insight is hot.
That’s how you ship nothing and burn the trust that got you here. The current thing still has to land. The client who’s paying for the bespoke system did not sign up to fund your platform epiphany, and the second customer who’d validate the product doesn’t exist yet. So you sit with the knowledge. You make the two backends agree by hand a little longer, you write down where the seams should go, and you resist the rewrite until the present commitment actually ships.
The contract job grew into a company while I wasn’t looking. The skill wasn’t seeing it. The skill was seeing it and not immediately wrecking the thing that was already working to chase the thing that wasn’t built yet.