Recalculating route: choosing an architecture without following every best practice
Friday evening, the satnav offers three routes and the internet its "recommended" one: microservices, Kubernetes, Kafka, Redis. Four junctions for a small, serious online shop, compromises written down with their exit condition, and the seatbelts that aren't up for negotiation.
Friday, 19 September 2025, 18:40. You're on the ring road, heading for the family olive mill up in the hills. The satnav offers three routes: "Fastest: 1 h 52, tolls", "No tolls: 2 h 04", "Shortest: 1 h 58, narrow roads". Above the first one, in green, sit the words that make you feel guilty no matter what: "Recommended route".
On your phone, in the team chat (two and a half people, and you're the half), the new online shop has been under discussion for days. By mid-October it has to take pre-orders for this year's new oil. October and November should bring a few thousand orders: the traffic of a small, serious shop. And the internet, like the satnav, already has its recommended route: microservices, Kubernetes, a dedicated message queue, Redis for caching, everything in the cloud. The famous best practices.
The satnav isn't stupid. It optimises one thing, minutes, for an average car. It doesn't know your tank is a quarter full, that the boot is packed with empty bottles, or that the old road has a café overlooking the lake. Best practices work the same way: they're excellent routes for someone else, often companies with hundreds of developers, millions of users and problems you don't have. Following all of them, always, is like taking the motorway to get to the bakery.
Tonight you have two hours of driving and four junctions to decide. Nobody's forcing you to take the recommended route.
18:52 · First junction: motorway or old road?
The first junction is the famous one. On one side, microservices: the application split into many small separate programs, each released on its own, talking to each other over the network. On the other, the monolith: a single program that does everything. Microservices are the motorway. They carry enormous traffic and let different teams drive in different lanes without treading on each other's toes, but there's a toll booth at every network call, tolls paid in servers and maintenance, and if you miss your exit you drive twenty kilometres before you can turn back.
Some who took the motorway without needing it have turned back, and written about it. In 2018 Segment, which routed data to dozens of external services, had more than 140 microservices, one per destination. In a post titled "Goodbye Microservices", Alexandra Noonan, from their engineering team, told how they merged them into a single service. One of the results: "We no longer had to deploy 140+ services for a change to one of the shared libraries."
In 2023 it was the turn of one piece of Prime Video. The tool that checks the audio and video quality of streams had been built "by the book" with AWS serverless services, passing frames to each other through S3 storage. According to the team, it could only handle around 5% of the expected load. They put it all back into a single process, and infrastructure costs dropped by 90%. Careful not to turn this into a rule too: it was one tool, not all of Prime Video, and Amazon hasn't given up on microservices. That's exactly the point: the same team, on that particular road, chose a different route.
And then there's the middle way, the fast dual carriageway. In 2019 Shopify, with one of the largest Ruby on Rails codebases in existence and more than a thousand developers who had worked on it over a decade, chose not to break it into microservices but to build a modular monolith: a single program, split into modules with strictly enforced boundaries. The same article has a line worth pinning up in the office: "no architecture is often the best architecture in the early days of a system."
For the oil shop, you take the dual carriageway. One program, with lanes painted on the tarmac: orders, inventory, payments. And to stop anyone cutting straight across from one lane to another on a Friday evening, you let a machine check the boundaries. In Python there's import-linter, which checks who may import whom:
[importlinter]
root_package = shop
[importlinter:contract:boundaries]
name = Inventory and payments don't know each other
type = independence
modules =
shop.inventory
shop.payments
[importlinter:contract:direction]
name = Nobody imports orders
type = forbidden
source_modules =
shop.inventory
shop.payments
forbidden_modules =
shop.ordersOrders may call inventory and payments; inventory and payments don't talk to each other and don't know orders exist. I tried it on a toy shop: all clear. Then I added the classic "just for something quick" shortcut to inventory, a from shop.payments import charge, and the check stopped like this:
Inventory and payments don't know each other BROKEN
Nobody imports orders KEPT
Contracts: 1 kept, 1 broken.
shop.inventory is not allowed to import shop.payments:
- shop.inventory -> shop.payments (l.7)That's the advantage of the road with painted lanes: if one day a module really does need to become a service of its own, its boundaries will already be clean, and detaching it will take weeks instead of months.
19:20 · Second junction: the service station
The satnav points out a big service station ten kilometres ahead. But you still have half a tank, and that's enough.
In the shop, the big service station is dedicated infrastructure. After every order there are jobs to do "shortly after", without keeping the customer waiting: the confirmation email, the shipping label. They go into a queue, and something works through it. Best practice says: a dedicated messaging system, like RabbitMQ or Kafka. The compromise says: you already have a PostgreSQL database, use a table.
CREATE TABLE jobs (
id bigserial PRIMARY KEY,
kind text NOT NULL, -- 'confirmation-email', 'label'...
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
done_at timestamptz
);
CREATE INDEX jobs_todo ON jobs (id) WHERE done_at IS NULL;And each worker, in a loop, does this:
BEGIN;
SELECT id, kind, payload FROM jobs
WHERE done_at IS NULL
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;
-- ...send the email, print the label...
UPDATE jobs SET done_at = now() WHERE id = $1;
COMMIT;The trick is FOR UPDATE SKIP LOCKED: it locks the row you take and skips the ones others are already working on. It's like a café counter: each barista takes the first ticket nobody else is holding. This isn't misuse: the PostgreSQL documentation itself says it can be used to avoid lock contention with multiple consumers accessing a queue-like table. And if the email fails, the transaction rolls back, the row is unlocked and the job goes back in the queue on its own.
I tested it on PostgreSQL 17: 2,000 jobs, three workers in parallel and 5% simulated failures (110 "failed" emails). Result: all 2,000 jobs completed, each exactly once, split almost evenly (650, 681, 669), with the 110 failures picked up again automatically. For a few thousand orders a month that's plenty. If one day hundreds of jobs a second arrive, you'll pull into the service station. That day, not today.
Same reasoning for the cache, the fast memory where you keep things you read often, like the price list. Best practice: Redis. Compromise: a dictionary that forgets.
import time
class ExpiringCache:
"""A dictionary that forgets. Per process: three processes, three caches."""
def __init__(self, seconds):
self.seconds, self.data = seconds, {}
def get(self, key, compute):
value, expires = self.data.get(key, (None, 0))
if time.monotonic() >= expires:
value = compute()
self.data[key] = (value, time.monotonic() + self.seconds)
return value
price_list = ExpiringCache(seconds=300)
def prices():
return price_list.get("prices", load_prices_from_db)In the test, a thousand reads of the price list make a single database query. The price you pay is written in the class's first line: each process has its own cache, so with three processes they could show different prices for up to five minutes. For a price list that changes twice a year, that's acceptable. For stock quotes, it isn't. And that's what a compromise is: a price you know about and decide to pay.
If this sounds amateurish, look at Stack Overflow, one of the most visited sites by developers worldwide. In 2016 Nick Craver, then on their team, published the full list of machines: 11 web servers (9 for the site proper), 4 SQL servers, 2 Redis, 3 Elasticsearch. On 9 February that year alone they had served more than 66 million page loads. Yes, they had Redis too: the compromise isn't giving up everything, it's taking only what you need and making it work superbly.
19:45 · Third junction: your own car or a rental?
Here the compromise changes direction, and it's worth a close look.
For years the best practice was "everything in the cloud". Then 37signals, the company behind Basecamp and HEY, did the opposite: it bought its own servers and moved almost entirely off Amazon's services. In October 2024 David Heinemeier Hansson, the company's CTO, did the maths in public: the cloud bill went from about $3.2 million to about $1.3 million a year, with estimated savings of over $10 million across five years. Those are the company's own estimates, not audited accounts, but the point is clear.
So should you buy servers too? No. 37signals has people who've looked after machines for twenty years and a predictable load. Your team is two and a half people, and nobody wants to be woken at 3 a.m. by a failed disk. For you, the toll is worth paying: a managed PostgreSQL database with automatic backups, and a single application server. Same question, opposite answer. The right compromise depends on your car, not on the road.
20:10 · Roadworks
A sign appears on the display: "Roadworks ahead, possible delays". Software has roadworks too, and they're called "new version". PostgreSQL 18 is planned for next Thursday, 25 September, and brings tempting new things, such as the new asynchronous I/O subsystem. But the shop opens in mid-October, and launch day is not the moment to try freshly laid tarmac. You start on 17, which you know, and upgrade in January, once the oil is sold. That's a compromise too: you give up something better, for a while, in exchange for one less surprise.
20:25 · Recalculating route
Somewhere between one thought and the next, you miss your exit. The satnav doesn't scold you. It just says: "Recalculating route". It's the wisest feature it has.
In 2016, in the annual letter to shareholders, Jeff Bezos split decisions into two types. Some are "one-way doors": irreversible or nearly so, to be made methodically, slowly, with consultation. But most are "two-way doors": they can be changed, you can go back through them. Those should be made quickly, by small groups with good judgement.
Look at tonight's choices with those eyes. The modular monolith is a two-way door: clean boundaries exist precisely so you can detach a module tomorrow. The queue in the database is two-way: it lives in a single module, and changing it touches a few lines. The cache, even more so. The one-way doors are elsewhere: the shape of your customer and order data, which payment provider you use, what you promise customers. There, yes, slow down.
And above all, write every compromise down. Four lines are enough, like a sticky note on the dashboard:
DECISION job queue in a PostgreSQL table
WHY a few thousand orders a month; one less moving part to run
COST no fancy priorities; a little extra load on the database
REVISIT above 50 jobs a second, or if the database starts strugglingThe line that matters is the last one. A compromise with an exit condition is a decision. A compromise without one is a debt nobody remembers taking on.
Seatbelts are not a compromise
Careful, though. Not being rigid doesn't mean everything is negotiable. You can take the old road instead of the motorway, but you still buckle up. In the oil shop, these are the seatbelts:
- backups, and a real restore test, at least once, before launch: a backup never restored is just a hope;
- HTTPS everywhere;
- payments handled by a specialist provider: card numbers must never pass through your servers;
- passwords stored with a purpose-built algorithm (bcrypt, Argon2), never in plain text;
- security updates applied, without waiting for January.
Flexibility is for choosing where to save, not for saving everywhere.
20:58 · Arrival
You reach the mill at 20:58. You didn't take the fastest route, or the shortest, or the recommended one. You took yours, with a stop at the café by the lake and one recalculation for the missed exit. The bottles are intact, and the tank held out.
Before getting out you write in the team chat: "Modular monolith, managed PostgreSQL 17, queue in the database, in-memory cache. All the seatbelts. The compromises are in the DECISIONS file. We'll revisit in January." Two and a half thumbs up, because you're the half.
Before you choose the road
A checklist to keep in the glovebox.
- Who was this best practice made for? How many people, how many users, what problems did its inventors have? Are they yours too?
- How much traffic do you really have? Today, and realistically in a year, not in ten.
- Who fixes it at 3 a.m.? Every extra piece is someone who has to know how to repair it.
- Is it a two-way door? If yes, decide quickly. If not, slow down and ask around.
- What does the compromise cost you? If you can't write it in one line, you haven't understood it yet.
- When will you revisit it? Write down the condition: a number, a date, a symptom.
- What's non-negotiable? Backups, security, customer data: those don't go on the scales.
- Does the new piece solve a problem you have, or one you've read about?
Sources
- Alexandra Noonan (Segment), Goodbye Microservices: From 100s of problem children to 1 superstar (10 July 2018)
- InfoQ, Prime Video's monitoring service moves from serverless to ECS (3 May 2023) and DevClass, Reduce costs by 90% by moving from microservices to monolith (5 May 2023)
- Kirsten Westeinde (Shopify), Deconstructing the Monolith (21 February 2019)
- Nick Craver, Stack Overflow: The Architecture – 2016 Edition (17 February 2016)
- The Register, 37signals' savings after leaving the cloud (21 October 2024)
- Jeff Bezos, 2015 letter to shareholders (published 2016), Type 1 and Type 2 decisions
- PostgreSQL, SELECT … FOR UPDATE SKIP LOCKED documentation, PostgreSQL 18 Beta 1 (8 May 2025) and PostgreSQL 18 RC 1 (4 September 2025) announcements
- Import Linter documentation