The choices that hurt later are all made in the first fortnight.
Most of what people call scaling problems are not about traffic. They are decisions taken in the first two weeks, when the product had no customers and the schema fitted on a whiteboard, that become structural once real data is in.
These are the five we take most care over, because we have paid for getting them wrong.
Whether tenants share a schema, get their own, or get their own database, is not a preference. It determines how you back up, restore, migrate and delete a single customer, and how you prove isolation to a buyer who asks.
The trap is starting with a shared schema and no tenant column on some early table, because at the time there was only one customer. Every query written afterwards inherits the ambiguity, and the fix is a data migration under load.
Put the tenant identifier on everything from the first migration, even while there is one tenant, and make it impossible to write a query without it.
Sequential integers in URLs tell every customer how many records you have and invite them to try the neighbouring number. Access control should stop that, and it will — until the one endpoint where it does not.
Use opaque identifiers externally. Keep integers internally if you like them for indexing, but do not put them in a URL or an API response.
The first version is two roles and an `if`. Then a customer wants a branch manager who can see two sites but only bill for one, and the `if` becomes a thicket nobody dares refactor.
Permissions belong in data — roles, scopes and grants that an administrator can configure — checked in one place. It costs perhaps two extra days at the start and saves rewrites every time a customer's structure differs from the one you imagined.
Background work will be retried: a deploy lands mid-run, a queue redelivers, someone clicks twice. If sending an invoice twice sends two invoices, that is a design fault rather than bad luck.
Make jobs idempotent from the beginning and give each unit of work a key you can check. Retrying safely is the difference between a queue that heals itself and one that needs a human at two in the morning.
Store timestamps in UTC, store the user's timezone separately, and convert only at the edges. It sounds obvious and is still the most common defect we find in systems we inherit — usually surfacing as appointments an hour out after a daylight-saving change, or a report that disagrees with itself across a month boundary.
For anything scheduled by humans, also store what they actually chose. "Nine in the morning, local" and "the UTC instant that was nine this morning" diverge the moment the clocks change, and the patient expects the former.
None of these are clever. They are all cheap on day one and structural by month six, which is exactly the profile of a decision worth slowing down for.
Everything else — the framework, the hosting, the language — you can change later with effort. These five you mostly cannot.
These are the five we settle at the start of every Software Development engagement, before the first table exists — and the same decisions sit underneath the nine products we already run in production.
We build the systems behind this — nine of them, running in production. Book a 45-minute walkthrough and we will show you how it works against your own workflow, or tell you honestly if you do not need it yet.