The three constraints that actually decide it
1. Who will maintain this in eighteen months?
The most important input, and the one most often skipped. If you will hire locally, choose what people near you know. If an agency builds it and hands over, choose something a generalist can pick up. If it is one technical founder, choose what they are fastest in — their velocity is the entire project.
An unfashionable stack that three available engineers know beats an elegant one that only the original author understands.
2. What does the product actually do?
Most products are forms, lists, permissions, and notifications over a relational database, and are well served by any mature web framework. A minority have a genuine technical centre of gravity — real-time collaboration, heavy background processing, on-device inference, low-latency audio — and that centre should drive the choice.
The mistake is letting a requirement that applies to 5% of the product dictate 100% of the stack.
3. How much operational load can you carry?
Every service you run is something to patch, monitor, and debug. A two-person team running six managed services is fine; running six self-hosted ones is not. Count the things that can page you, not the things in the architecture diagram.
Defaults worth defending
Unless something specific argues otherwise, these are the defaults we start from and the reasons why:
- A relational database. Your data is relational. Postgres handles JSON, full-text search, and geospatial queries well enough that most products never need a second store. Adding one later is easy; removing one is not.
- A boring, batteries-included backend framework. Auth, migrations, admin, and background jobs solved by convention rather than assembled from ten packages.
- One language across the backend where practical. Context switching has a real cost on small teams.
- Managed infrastructure. Managed database, managed queue, managed object storage. The premium is far less than an engineer's time.
- Server-rendered UI unless the product needs rich client state. Many products carry the complexity of a single-page application for interactivity they do not have.
These are defaults, not rules. The point is that departing from them should require an argument.
Choices that are expensive to reverse
Not all decisions carry the same weight. These are the ones worth extra thought, because changing them later is a project rather than a refactor:
| Decision | Cost to reverse |
|---|---|
| Primary database and data model | Very high — migration with live data |
| Identity and tenancy model | Very high — touches every query |
| Native vs cross-platform mobile | High — effectively a rebuild |
| Backend language | High, but service-by-service |
| Frontend framework | Moderate — usually page by page |
| Hosting provider | Low if you avoided proprietary primitives |
| CSS approach, component library | Low — argue about this less |
Spend your deliberation budget at the top of the table. The bottom rows attract the most debate and matter the least.
On mobile specifically
The native-versus-cross-platform question is the most consequential mobile decision, and the honest answer depends on one thing: how much of your product is device capability.
Cross-platform is the right default for products that are mostly screens, forms, and networking — the majority. It roughly halves the build and maintenance for the same surface area.
Native earns its cost when the device is the product. A custom audio engine with precise playback control, or a connected-camera product streaming live video with location tracking, will spend more time fighting a cross-platform bridge than it would have spent writing platform code.
The test we apply: list the features that touch hardware or platform APIs in a non-trivial way. If that list is short and peripheral, go cross-platform. If it is the reason the product exists, go native.
Keep the exits open
Since you will get some of this wrong, the practical goal is to keep reversals cheap:
- Keep business logic out of framework-specific classes. Plain functions and modules move; framework internals do not.
- Wrap third-party services behind a thin interface you own — payments, email, storage, and model providers especially.
- Avoid proprietary primitives with no equivalent elsewhere unless the benefit is large and named.
- Keep the schema clean and documented. Data outlives every framework decision you will make.
None of this is about anticipating the future correctly. It is about making sure being wrong costs a sprint rather than a quarter.
How to actually decide
A short exercise that resolves most stack debates in an hour:
- Write down who maintains this in eighteen months.
- List the three features with real technical difficulty.
- Count the services you are willing to be paged for.
- Choose the most boring stack that satisfies all three.
- Write a one-page note recording the decision and its reasoning.
That last step is the one people skip and the one that pays off. Six months later, when someone proposes a change, the note tells you whether the original reasoning still holds — which turns a recurring argument into a five-minute check.