Intrinsic vs Extrinsic.


Systems can be as complicated as the human mind can devise. And humans are very good at devising.

There is a difference between intrinsic and extrinsic complexity. Writing non-trivial software systems has a certain complexity that is inescapable if the business logic it’s trying to support is complicated. That is intrinsic. The last thing we need is to add in more complexity in the tech stack. That is known as extrinsic complexity, and it is something we should always try to avoid.

There is no shortage of moving pieces that can be introduced: queues, caches, distributed async systems, multi-stage transactions, service-oriented architecture, enterprise service buses, microservices, event sourcing. Each has its place. Most of those places are at Google or Facebook. If you’re not them, reaching for these tools rarely solves the problem you have, and reliably introduces problems you didn’t.

The largest trucking companies in this country are mid-market businesses. The same is true of most regional banks, most distributors, most manufacturers. The systems that run these companies don’t need to scale to a billion users. They need to be reliable, maintainable, and small enough that a small team of engineers can hold the whole thing in their heads. That’s a different design constraint, and it leads to different answers.

So what does the boring, conservative stack actually look like? Here’s mine.

Start with PostgreSQL. A good relational database solves more problems than most teams realize, and Postgres is the default choice in 2026 for reasons that aren’t going away. It handles the workloads of any mid-market business comfortably. It does JSON when you need it, full-text search when you need it, and even job queues when you need them. The teams I’ve seen reach for MongoDB, DynamoDB, or some specialized store almost always end up rebuilding relational features on top, badly. I’ve seen pages of Java code against a major vendor’s NoSQL project replicating what should have been a trivial SQL query. Use Postgres until you have a specific reason not to.

One or two application servers running your business logic. Node and JavaScript are my preference because the language runs everywhere and the ecosystem is enormous. Java and Go are perfectly reasonable alternatives if your team leans that way. The choice matters less than people think. What matters is having a single codebase that a new engineer can run on their laptop within an hour of joining.

Skip the microservices. A modular monolith — one application, well-organized internally — is faster to build, easier to debug, and easier to deploy than the equivalent system split across six services. Microservices are tempting in that one team can go and build, say, a credit check microservice very quickly, because no coordination with other teams is needed. The complexity will come later when it is time to start making everything work together. If you build a monolith you can always extract services later if you find a real reason to. Most teams never do.

For the UI, React is fine. It’s not the best framework on technical merit and it never was, but it has the largest hiring pool and the most mature ecosystem, which matters more. Pick a component library — Chakra, shadcn, MUI, whatever — and stop debating it. None of these choices materially affect business outcomes.

Integration is glue code. Mid-market companies always need to talk to other systems — TMS, CRM, accounting, EDI partners, vendor APIs. The temptation is to buy a commercial integration platform. These tools are expensive, require specialized skills to operate, and still require you to write transformation code. I worked at a company where the team in charge of the SOA integration platform was as large as the core development team, and couldn’t help but notice that every integration project still needed developers to write the code that integrated to the target systems. You can write the same glue code yourself in a fraction of the time, with no licensing, and a junior engineer can maintain it. If you need a queue you can use a capable but minimal queue service, such as RabbitMQ, or even Postgres, for a tiny fraction of the price of a full-fledged service bus or similar.

Run it on cheap infrastructure. A managed Postgres instance, a couple of application servers behind a load balancer, an object store for files. AWS, GCP, Azure, Hetzner, whatever. The bill should be measured in hundreds of dollars a month, not tens of thousands. If your infrastructure is more expensive than your senior engineers, something has gone wrong.

That’s the whole stack. A two-pizza team can build, deploy, and maintain almost any mid-market business system on it. None of these choices are exciting. None will land you on the front page of Hacker News. They will, however, still be running in five years.

I’ve seen sophisticated architectures die after years of heavy consultant spend, because the team that inherited them couldn’t keep them running. I’ve seen unfashionable systems trudge along for decades, because someone made conservative choices and the company never had to relearn its own infrastructure. The difference was not intelligence or budget. It was how much extrinsic complexity each team chose to take on.