Choosing a Web Technology Stack You Can Still Run in Five Years
Why we build back ends on Java and Spring Boot, why this site has no framework at all, and how to tell which of those two situations you are in.
Most stack debates are argued on the wrong axis. The question is rarely which technology is best. It is which one you will still be able to run, staff and patch in five years, on the budget you will actually have then rather than the one you have during the project.
That reframing changes most of the answers.
The cost that shows up later
A technology choice has two costs. The first is building, and it is the one everyone estimates. The second is keeping the thing alive: security patches, dependency upgrades, and the ability to hire someone who can safely change it.
The second cost is larger and it is paid every year. It is also almost entirely determined by decisions made in the first fortnight.
A concrete version of this: a JavaScript project with a large dependency tree accumulates vulnerability advisories continuously. Most are irrelevant to your threat model, but someone has to look at each one to know that. Multiply by a few years and the maintenance burden of a dependency-heavy stack is not a rounding error. It is a standing commitment nobody costed.
Where we use Java and Spring Boot
For systems that handle money, statutory calculations, or records that must still be correct years from now, we build on Java and Spring Boot. The reasons are unglamorous:
- Decimal arithmetic that is correct by default.
BigDecimalwith an explicit scale and rounding mode. In financial software this is not a preference. Floating point is simply the wrong tool, and a platform that makes the right tool the obvious one prevents a whole category of defect. - A type system that catches structural mistakes. Refactoring a large domain model without one is an act of faith.
- Genuinely long support horizons. LTS releases are supported for years, so an upgrade is something you schedule rather than something that ambushes you.
- A hiring pool that exists locally. This is the one people underweight. A system nobody available can maintain is a liability regardless of how elegant it is. In Lusaka, Java developers are findable; specialists in a fashionable framework from two years ago are not.
- The boring parts are solved. Spring Security, Spring Batch and Spring Data cover authentication, scheduled batch work and persistence without inventing anything.
The honest counterpoint: Java is verbose, the startup footprint is heavier than a scripting runtime, and for a small CRUD application it is more machinery than the problem deserves. Which brings us to the other half.
Where we use nothing at all
This website is static HTML, CSS and JavaScript, generated at build time by a small Python script. No React, no Tailwind, no client-side router, no web fonts. The whole site is a handful of requests and well under half a megabyte.
It replaced a React application. It loads faster, there is dramatically less of it to maintain, and the security surface is close to nil because there is almost no third-party code to have vulnerabilities in.
That is not an argument against React. It is an argument that a content site was never the problem React was built to solve. The most expensive mistake in this area is not picking the wrong framework. It is picking a heavy one for a problem that never needed one, and then paying its maintenance cost forever for a page that displays text.
A test that usually settles it
Ask what the interface does when the user is not clicking.
If the answer is "nothing, it shows content until someone navigates", you are building a document and server-rendered HTML will serve you better on every axis that matters. If the answer involves live state, optimistic updates, complex client-side validation across interdependent fields, or a canvas, then you have a genuine application and a component framework earns its cost.
Most business software is a mix, and the mistake is applying one answer to the whole system. A rich internal dashboard and a public marketing site have no reason to share an architecture.
Databases
PostgreSQL unless there is a specific reason otherwise. Strong constraints, real transactions, a decimal type that behaves, and JSON columns when a structure genuinely does not want to be relational.
The reason to be conservative here is that the database outlives the application. Applications get rewritten; data gets migrated, painfully, and every schema shortcut taken early is paid for during that migration.
Be suspicious of reaching for a document store because the schema is "not settled yet". Schemas are never settled. The relational constraint is what tells you when a change breaks an assumption, which is precisely the thing you want to know early rather than in production.
Hosting and the local reality
Two constraints shape this more here than elsewhere.
Latency to the user. Servers in Europe or North America mean a round trip that is felt on every interaction. A CDN in front of static assets fixes most of it for content; for interactive applications, chattiness matters. An interface making twelve sequential API calls to render one screen feels broken at 250ms of latency even though each call is individually fast.
Bandwidth is a cost to the user. People are paying per megabyte, frequently on a phone. Page weight is not an abstract performance metric. It is money out of your visitor's pocket, and it is a genuine reason to prefer a 40KB page over a 2MB one beyond the load time.
The pipeline is part of the stack
Whatever you pick, these are not optional and are far cheaper to establish at the start:
- Version control with a branching model the team actually follows. The model matters less than the consistency.
- Automated tests where correctness is not visually obvious. Statutory calculations, permissions, money. Not a coverage target, but tests where being wrong is expensive.
- Reproducible builds. The same commit produces the same artefact on any machine. "It works locally" is a debugging cost paid repeatedly.
- One-command deployment. If releasing is a manual ritual, releases become rare, which makes each one larger and riskier.
- Error tracking in production. Not knowing something is broken is worse than it being broken.
Questions worth asking before committing
- Who maintains this in three years, and can we hire them here?
- How many direct dependencies, and who is behind each one?
- What is the support horizon for the runtime version we are pinning?
- How much of this framework are we actually using, and what would it cost to do that part without it?
- If this choice turns out badly, what does backing out cost?
That last one is the most useful. Some decisions are reversible in a sprint; others are effectively permanent. Spend the deliberation where reversal is expensive, meaning the data model, the runtime and the hosting shape, and pick pragmatically everywhere else.
If you are making this decision now and would like an outside view before committing, get in touch. It is a cheaper conversation before the code than after.