Customise by extension, never by forking
Every bespoke platform reaches a moment where the fastest way to satisfy a client is to copy the codebase. That decision is where products go to die.
The scenario is always the same. You have a working platform and three happy clients. The fourth wants something the model does not express — a different approval chain, an extra field that participates in a calculation, a rule that applies only in their state. The deadline is short. Copying the repository and changing the four files that matter would take an afternoon.
Do it once and you have two codebases. Do it four times and you have a services business pretending to be a product company, where every security patch has to be applied five times and no client can be upgraded without a project.
What extension actually means
The alternative is not "make everything configurable" — that road ends in an unusable admin screen with four hundred toggles. It is narrower: identify the specific axes along which clients genuinely differ, and make only those axes data.
- Approval chains differ → model the chain as rows, not as code.
- Field sets differ → allow defined extension attributes, typed and validated, rather than a free-form JSON blob.
- Calculation rules differ → name each rule, store which variant applies per tenant, keep all variants in the shared codebase.
- Terminology differs → a label table. Cheap, and it removes a surprising amount of pressure to fork.
The discriminator is whether the difference is structural or parametric. A client who calls a purchase order a "work order" is parametric. A client whose purchase orders can be raised against two projects simultaneously is structural, and needs the shared model to grow.
The hard part is saying no to the fast answer
When the structural case appears, the honest options are to extend the shared model so it can express the new case for everyone, or to decline the requirement. Forking is the third option that looks like a shortcut and is a mortgage.
Extending the shared model for one client is slower this month and is the reason the platform still exists in three years.
It also tends to improve the product. A requirement one client articulated clearly is usually a requirement several clients had and never expressed. Modelling it properly once often turns out to have been the roadmap.
Tenancy is the same problem, one level down
The instinct to isolate a client by copying the code is the same instinct that isolates them by copying the database. Both feel safer and both multiply your operational surface. A tenant boundary enforced in the schema — every row carrying the tenant, every query constrained by it — gives you isolation without giving you five databases to patch.
And it should be enforced below the application. If the application layer is the only thing preventing a cross-tenant read, then a single missing clause in a single query is a data breach. If the boundary is in the data model and the access path, a bug is a bug rather than an incident.
A test for whether you got it right
Ask how long it takes to onboard a client whose requirements you have not seen yet. If the answer involves a developer, a branch and a release, you have a bespoke system with several customers. If it involves configuration and a data load, you have a platform. The distinction is not academic — it decides whether client five is profitable.