Case study · Enterprise CRM
Nine years, one developer, and two services
A CRM for commercial property, kept in production for nine years while its backend platform, its frontend generation and its database were all replaced underneath it. The interesting decision was how little of it became services.
- Client
- A commercial real estate group in Russia
- Domain
- Enterprise CRM
- My role
- Sole developer, nine years
- Period
- 2014 – 2023
- Result
- Nine years in production, audience up 25%, two services extracted
Context
A CRM running commercial property rental: tenants, contracts, spaces, charges, acts, and the people who walk the buildings. Internal staff used it daily, and the business ran on it in the ordinary sense that if it stopped, work stopped.
I was the only developer on it for nine years. Architecture, backend, frontend, mobile, deployment. That single fact drove every decision below and is the reason this case is not the microservices story the title of the original plan suggested.
The problem
Nothing was on fire. That is worth stating, because the usual decomposition story starts with collapsed release cadence or a load profile that has outgrown one box, and neither happened here.
What happened was slower. The platform aged. A PHP and Laravel monolith with server-rendered pages was a reasonable 2014 decision and an increasingly expensive 2019 one, in library support and in what it could offer a user interface. Meanwhile the business wanted people on site to work from a phone while standing in a building, and that requirement does not fit into a server-rendered admin panel no matter how it is styled.
So the forcing function was replacement, not scale. And replacement had to happen with the system live, on one person’s calendar, without a period where the business could not work.
Constraints
No rewrite window, ever. Every intermediate state had to be a working system. Nine years of continuous production is not a decoration on this case, it is the constraint that eliminated every plan that starts with a branch.
One developer pays every operational cost, forever. A service is not free after it ships. It is a pipeline, a deployment, a log stream, a monitoring surface and a class of network failure, charged monthly to the same person. With a team of six you can absorb that. Alone, a service has to earn its existence against my own future attention, which is the scarcest resource in the project.
One database, and I kept it that way on purpose. The whole business reports against that schema. Splitting it would have meant answering, for every report, what happens to the joins, and the answer would have been duplication maintained by hand by one person. The coupling stayed, deliberately, with the costs paid knowingly rather than accidentally.
Field connectivity is bad. People inside industrial buildings and basements have no signal. Whatever the mobile client was, it could not assume a working network at the moment the user needed it.
Nine years means every choice is a nine-year liability. Reversing an architectural decision costs more than the decision saved. This is the argument against building for a scale you have not yet seen.
What I did
Replaced the platform in pieces, under load. PHP and Laravel gave way to Node.js on Hapi, endpoint by endpoint, with both stacks live in parallel while each area moved. The server-rendered frontend gave way to Vue 3 the same way, screen by screen. Later the database moved from MySQL to PostgreSQL under a running system. None of these were events with a date. They were long migrations where the system stayed usable on every ordinary Tuesday in the middle.
Kept the monolith as the default home. New functionality went into it unless something specific argued otherwise. This is the decision I would defend hardest: services were answers to demonstrated problems rather than a target state, so after nine years there are two of them.
Extracted the mobile API, because its client was genuinely different. Field staff on React Native, on bad connections, running whatever version they had last installed. That means payload shapes, error handling and versioning policy that have nothing in common with an internal desktop screen. Keeping it inside the monolith would have meant one codebase serving two clients with opposite requirements.
Extracted notifications and integrations, because they fail differently. External systems are down, slow, or wrong in ways your own code is not. Retries, backoff and dead letters belong somewhere they cannot take a user-facing request with them.
Automated deployment early and kept it boring. Release frequency improved over those nine years, and the credit goes to CI/CD rather than to the architecture. The pipeline is what made small changes cheap. Worth saying plainly, because decomposition often takes credit for what automation delivered.
What went wrong
Version skew, which is the incident I did not design for. Mobile clients in the field run the version they last installed, and a phone in a basement does not update. So an API change that was correct against every client I could see was wrong against clients I could not, and the system was briefly inconsistent with itself: a monolith serving one truth, an app on an older contract believing another. Splitting the mobile API created that failure mode. Inside the monolith it could not have existed.
Partial integration failures arrived with the split. An operation that used to succeed or fail as one unit now had a state where our side had recorded something and the external system had not, or the reverse. Most of the fixes were about moving the boundary so the two halves stayed together, rather than about building machinery to reconcile them afterwards.
Deployment got harder, and that cost is permanent. Three things to ship instead of one, three sets of logs, three ways for a release to be half done. The pipeline absorbed most of it. Not all of it, and the residue is paid every week by one person.
One database means a migration can hurt a neighbour. This is the price of the choice I described above as deliberate, and it is a real price: a schema change made for the monolith has to be checked against a service that reads the same tables. I would make the same call again at this team size, with the cost stated out loud.
Result
- Nine years in continuous production, with no rewrite window at any point.
- Backend platform, frontend generation and database engine all replaced while the system stayed in daily use.
- Total audience up 25 percent within a year of the mobile app launching, which came from reaching people who were never going to sit at a desk.
- Two services extracted in nine years, both still justified.
- Release frequency improved, from CI/CD rather than from decomposition.
The honest summary is that restraint was the engineering. A single developer who splits a system along a domain diagram ends up maintaining a distributed system alone, and the diagram does not help at 2am. Both extractions here happened because something was measurably hurting: a client with different requirements, and failures with a different shape. Everything else stayed together, and staying together is why one person could keep it alive for nine years.
Stack: PHP · Node.js · Hapi · Vue 3 · React Native · PostgreSQL · Docker