Every few years I get asked to look at a payments platform that has outgrown its first design. The pattern is always roughly the same. A large relational database, a single deployable in front of it, a team that has doubled twice, and a release process everyone has learned to be frightened of. Someone has written a paper proposing microservices. Someone senior has already said the word “distributed” in a board meeting.
The paper is usually right about the problem and wrong about the fix.
The problem is release coupling, not the monolith
What has actually gone wrong in these systems is rarely the single deployable. It is that nothing inside it has a boundary. Any module can reach into any table. A change to how fees are calculated touches the same code path as a change to how agents are paid, so the two cannot ship independently, so they ship together, so every release carries the combined risk of everything anyone did that fortnight. Twelve teams end up sharing one deployment slot and one rollback.
That is a real and expensive problem. It is also not evidence that the system should be distributed, because splitting into services fixes it by accident rather than by design. You get independent deploys, and you pay for them with a distributed system you now have to reconcile.
What splitting costs in payments specifically
In most domains, a service boundary means a call that might fail and a retry that might duplicate. You handle it, you move on. In payments, a call that might fail and a retry that might duplicate is money that might move twice.
Take the simplest transaction in the business: a customer sends money to another customer. Debit one balance, credit another, write a ledger entry, publish something so the notification goes out. In one database that is a transaction. It commits or it does not, and the ledger is the truth.
Split the balances into a wallet service and the ledger into its own, and the same operation becomes a saga. Debit succeeds, credit times out. Did it apply? You do not know, so you retry with an idempotency key, which means every participant now needs idempotency keys, and a store of them, and an expiry policy for that store. Then you need a compensating transaction for the case where the credit genuinely failed, which is not a refund, because a refund is a real business event with its own ledger entry and tax treatment. Then you need to decide what the customer sees during the window where the money is in neither balance.
None of this is impossible. All of it is work you did not have before, and most of it is work that only shows up under load or under failure, which is to say in production, at the worst possible time. The reconciliation job you write to find these cases becomes one of the most important programs in the company, and nobody wants to own it.
I have watched teams take this on for the right reasons and get through it. I have also seen the version where the migration stalls halfway, and the company runs a monolith and a service mesh at the same time, with the ledger split across both. That is the worst configuration available, and it is where these programmes go when the organisational pressure that started them moves on.
What the largest one actually did
The biggest mobile money platform in the world ran a monolith for ten years and then rebuilt it, and both halves of that are on the public record.
M-PESA moved onto a new platform in 2015 and evolved one code base from then until 2025. Felix Rop, Safaricom’s Head of Financial Services Technology, describes the starting point without euphemism: “We started with a monolithic system, basically to mean that every change was really a headache, just to make a simple change, not highly configurable.” In 2020 they added a handful of microservices for critical services and left the architecture otherwise intact.
What forced the rebuild was not the code. It was a write ceiling. Rop again:
It’s an architecture that could only scale to 5,000 transactions per second, and it is because of the database limitation that we had. And therefore we took a strategic decision to say, why can’t we shard the databases in this case? Why can’t I have a segment of customers being on database X, database Y, but also ensure that all of them are talking to each other seamlessly? From an application perspective, why can’t we go the microservice route?
Read that ordering carefully, because it is the whole thing. The database decision comes first and separately, and it is sharding by customer, not decomposition by function. A customer’s transactions still resolve inside one shard. The microservices arrive afterwards, and they arrive in the application tier.
They did not carve the ledger into a service and reach for a saga. They kept the transactional model and cut it into segments that each still hold one.
Two other mechanisms are worth stealing outright. The queues were split by transaction type and by partner, because, in Rop’s words, “there was a time when one partner going down would affect all the other partners; when you get three partners going down they bring down the whole M-PESA.” That is bulkheading a periphery so it can fail without taking the core with it. And balances are chained: “your previous balance forms the series of the next balance,” so a write that goes round the application breaks the chain and the account fails closed on its next use.
The cost is public too. Around 300 person-years, starting December 2024. Two full dress rehearsals against live counterparties, including live bank partners. A deliberately minimal test suite on the night, because the window was three hours. Fourteen days’ notice to the central bank. Services came back at about four in the morning on 22 September 2025, half an hour past the advertised window, on a platform serving tens of millions of people.
What is not public is as instructive as what is. Nobody has said what the database is, what the services are written in, or what the consistency model looks like. There is no public statement about sagas, two-phase commit or idempotency anywhere in the record. And there is no account of a parallel run, shadow traffic or a documented rollback. The described method is a timed cutover with a rehearsed data migration, which is not how most of us would advise doing it and which they clearly earned the right to do through rehearsal rather than confidence.
I am reading the same public sources as anyone else here, and I have a stake worth declaring: I advised M-PESA on an earlier iteration of exactly this question. What I take from how it landed is that the instinct to protect the transaction survived contact with a scale almost nobody else operates at.
The structured monolith
The thing that actually fixes release coupling is boundaries, and boundaries do not require a network.
A structured monolith is one deployable with enforced internal modules. Each module owns its tables and nobody else reads them. Modules talk to each other through published interfaces, in process, synchronously, inside the same transaction when the domain calls for it. The import graph is checked in CI, so the boundary is not a convention that erodes the first time somebody is in a hurry.
That gets you most of what the services proposal was really after. Teams can reason about their own code. A change to fees does not compile against agent payouts. Ownership is clear enough to put a name on. And when something does need to leave the process, because it has genuinely different scaling or availability characteristics, you extract a module that already has a clean interface instead of carving a seam through live code.
The part people find hard to accept is that the ledger should be the last thing to leave, and probably should never leave. Money movement wants one writer and one transaction. Everything else in a payments platform, and there is a lot of it, can go: notifications, statements, reporting, fraud scoring, agent tooling, reconciliation itself. Those have different failure tolerances and different scaling shapes, and losing one for ten minutes is not the same class of event as losing the ledger.
So the design I keep arriving at is a small hard core that moves money, in one place, with one database, and a wide periphery of things that can fail independently around it. That is not a compromise between monolith and microservices. It is a statement about which parts of the business can tolerate eventual consistency and which cannot.
Python, and the objection to it
A lot of these platforms are Python, and at some point in every review someone suggests that this is the real problem and it should be rewritten in something faster.
It almost never is. The systems I have looked at were not CPU bound. They were waiting on a database, a card network, a telco gateway or a partner bank, and the slow part was somebody else’s. Rewriting the language would have moved a bottleneck that was not there.
What Python does do is let a boundary rot quietly, because nothing stops a module reaching into another module’s models at runtime. So the discipline has to be imposed rather than assumed. Types everywhere, checked in CI and treated as a build failure, not a warning. Explicit module interfaces with everything else private. An import linter that fails the build when the boundary is crossed. Migrations owned by the module that owns the tables.
That combination is the same discipline a compiled language hands you for free in one dimension and withholds in the others, and it is cheap once it is in the pipeline. The expensive version is the one where you decide to be careful and rely on review to enforce it.
SRE when the thing you run moves money
The operational side is where I think the most value sits, and it is the part that gets least attention in the architecture paper.
Availability targets for payments are the wrong first question. The right one is what happens to a transaction that was in flight when the thing fell over. A platform that is up 99.99% of the time and loses in-flight state on restart is worse than one that is up 99.9% and can tell you, for any transaction, exactly which stage it reached. Recoverability beats uptime, because uptime is a number you report and recoverability is a property customers experience.
That pushes a few things to the front. Every money-moving operation gets a durable record of its own progress before it starts, not after it finishes. Every external call is idempotent by construction, with the key derived from the business event rather than generated at the call site. The reconciliation job needs an owner and its own alerting. A failed run is worth knowing about; a run that finds a discrepancy is worth waking someone for.
Instrumentation belongs in the design, not bolted on afterwards. I have made telemetry review part of the definition of done for years: if you cannot say how you would tell this was broken at three in the morning, it is not finished. In payments that means tracing that follows the money rather than the request, which is a different span structure from the one you get out of the box, because a single transfer can outlive several requests.
And the change process itself is part of the system. Blocking quality and security gates in the pipeline rather than advisory ones. Deployment that can roll back a schema, which usually means expand and contract migrations and a period where the code tolerates both shapes. None of this is novel. It is just rarely funded, because it does not appear on a roadmap as a feature.
What I would actually do
If someone handed me one of these platforms tomorrow, the first thing I would do is not choose an architecture. It would be to find out what the deployment fear is made of, because that is the thing everyone is really complaining about, and it is usually three or four specific coupling points rather than a general property of the design.
Then boundaries inside the existing deployable, enforced by the build. Then extract the periphery, starting with whatever has woken someone up most recently. Then leave the ledger alone until there is a reason that survives being written down.
It is slower than a rewrite in the first quarter and considerably faster by the fourth, and it has the advantage that every step of it ships. The rewrite has one moment where it either works or you find out in public how good your reconciliation is.