Every few years, someone in the room says it. Maybe it is the new CTO. Maybe it is a consultant. Maybe it is you, staring at a model that has been accumulating complexity since before the Global Financial Crisis. The words are always roughly the same: “We should rewrite this.”
The sentiment is understandable. The existing model is slow, poorly documented, and full of code that nobody fully understands. There is an opaque subroutine that handles a tranche of conventional with-profits business written in the 1980s, and the only documentation is a comment that reads: “Whatever you do, do not remove this.” The codebase is not so much software as it is sedimentary rock, and each layer contains its own fossils.
So you start fresh. Clean architecture. Modern platform. This time, you tell yourself, we will do it properly.
Joel Spolsky wrote about this pattern over two decades ago, calling it the single worst strategic mistake a software company can make. Netscape rewrote their browser from scratch and nearly died. More recently, Sonos rewrote their mobile app from the ground up in 2024, promising “an unprecedented streaming experience.” The result was a catastrophe: core features were missing, existing systems broke, the company’s share price dropped by 25%, and the CEO lost his job. The rewrite had taken two years. The team had done extensive user testing. The prototypes looked good. The problem was not the vision. It was the thousand things the old app had quietly been handling that nobody had thought to write down.
The complexity is in the problem, not the code
When a developer looks at a legacy actuarial model, they see spaghetti and hacks and massively nested if statements and think “bad code.” Sometimes they are right. But more often, the cludge is an accurate representation of complicated business logic. That weird branch that treats one closed book differently from every other? That is a tranche of policies migrated in from another licence in some distant corporate past, policies that stubbornly crawl off the books at their own pace rather than running off as the assumptions suggest. The hardcoded matrix of flags in the decrement logic? That is handling multiple causal surrender and lapse events, where the interaction effects were agreed through industry negotiation rather than derived from first principles.
Consider the products themselves. Conventional with-profits policies from the 1970s and 1980s, now-defunct retail discretionary smoothed bonus products, universal life variants with guarantee structures that differ by tranche because of policy wording glitches that affected business written during a narrow window. These are not edge cases. In a mature life insurer’s book, they are the book. Every product variant that was ever sold, however briefly, however accidentally, lives in the model forever.
You can refactor the syntax. You can rename the variables. You can restructure the control flow. You can attempt to enforce coding standards. The essential complexity survives every rewrite, because it was never a function of the code to begin with. It was in the contracts, the policy wordings, the historical decisions, and the accumulated institutional memory of the actuarial team.
This is why model rewrites so often converge back towards the same complexity as the original. The optimistic timeline assumes the new model will be simpler. It will not be simpler. It will be the same complexity with less battle-testing.
The rabbit hole goes deep
I recently fell down a rabbit hole reading about why COBOL systems persist in banking. The reason turned out to be more interesting than I expected, and it illustrates something important about legacy systems in general.
COBOL performs arithmetic in decimal. Not binary floating-point, which is what R, Python, C, and virtually every modern language use by default, but actual base-10 arithmetic. This matters because in binary floating-point, 0.1 cannot be represented exactly. It is not a rounding issue or an implementation bug. One-tenth in base 2 is a repeating fraction, the same way one-third is repeating in base 10. The practical consequence is that 0.1 + 0.2 does not equal 0.3 in any language that uses IEEE 754 doubles. Try it in R. It returns FALSE. Excel, incidentally, will tell you it returns TRUE, because Microsoft decided decades ago to silently paper over the issue with heuristic rounding at the display level. It is a load-bearing lie: technically dishonest, pragmatically essential, and the foundation upon which the entire global financial system’s spreadsheets rest.
For actuarial modelling this rarely matters in practice. You are working with estimates and probabilities, and 15 significant digits of precision is more than enough. But the point is not about COBOL or Excel specifically. It is about how deep the assumptions in any system go. The banking world discovered, when they tried to move off COBOL, that the decimal arithmetic was not just a feature of the language but a load-bearing property of the entire ecosystem. Rounding behaviour, reconciliation logic, audit trails: all of it assumed exact decimal representation.
Actuarial models have their own version of this. Assumptions about evaluation order, about when intermediate rounding occurs, about how decrements interact, about the precise sequence of operations in a monthly projection step. These assumptions are rarely documented because the people who wrote the model did not think of them as assumptions. They were just “how it works.” Until someone tries to replicate “how it works” in a new environment and discovers that a hundred small implicit choices produce a hundred small differences, each individually immaterial, collectively significant, and individually painful to diagnose.
The documentation mirage
The standard diagnosis at this point is: “Well, we should have documented it properly.” And yes, in an ideal world, every modelling decision would be recorded, every assumption justified, every edge case explained. But documentation has a half-life. The moment you write it, it starts decaying. And the effort to keep it current competes with the effort to actually do the work it documents.
Models most in need of documentation are the complex ones that change frequently, which are precisely the ones where documentation goes stale fastest. The simple stable model that has not changed in five years has beautiful documentation. The critical model that three people are iterating on weekly has a README from 2019 and some optimistic comments about “come back to this” in the code.
Good documentation requires a different skill from good modelling. Explaining why you made a choice is harder than making the choice. Most documentation ends up describing what the model does, which anyone with the code can see, rather than why it does it that way, which is the thing that is actually lost when someone leaves.
So when someone says “we will document the old model thoroughly before we migrate,” treat that with the scepticism it deserves. It is not that documentation is worthless. It is that comprehensive documentation of a complex actuarial model is closer to a research project than a task on a Gantt chart, and it will never be truly complete. The knowledge that matters most, the kind that explains why line 437 exists, often lives only in the model itself and in the memories of the people who built it. Sometimes the most valuable documentation in the entire codebase is a comment that says “do not remove this.” It tells you nothing about what the code does. It tells you the one thing that matters: someone before you tried removing it, and something terrible happened.
The testing problem is nearly intractable
The textbook software development answer to safe migration/refactoring is: write comprehensive tests against the old system, then verify the new system produces the same results. This sounds clean. It falls apart quickly in practice.
An actuarial model is not best thought of as a simple function that takes an input and returns an output. It is a function of data, assumptions, methodology, regulatory basis, reporting date, and a constellation of configuration choices. To truly test it, you would need to verify results across combinations of all of these dimensions: base and stressed assumptions, multiple products, different data vintages, BEL, SCR, RM, RA, EV, ORSA projections and more. The required test suite is, for practical purposes, infinite.
But even if you could write enough tests, testing locks in current behaviour, not correct behaviour. If the existing model has a subtle bug that slightly misstates the impact of a particular stress scenario, your tests will faithfully preserve that bug. You now have a comprehensive test suite that gives you high confidence in reproducing the wrong answer. Which is arguably worse than no tests, because it creates false certainty.
Conversely, tests that are too simple create their own risks. A test that says “the total reserve is within 1% of the old model” might pass while masking a situation where two large errors in opposite directions cancel out. Over time, as the portfolio changes and one of those errors stops being offset, the discrepancy surfaces, and by then nobody remembers that the migration was the cause.
The practical approach is something looser than formal test suites: attribution analyses, movement analyses, and sense checks that tell you why results changed, not just whether they changed. An unexplained difference is not a rounding tolerance to be waved through. It is an unknown.
The replication question
Here is a question that derails more migration projects than any technical challenge: during the rebuild, the team discovers that the old model has a bug. Maybe the lapse rates for a particular cohort are being applied incorrectly. Maybe a decrement interaction is not handling concurrent events properly. The old model has been producing results with this error for years, and those results have been reported, audited, and relied upon.
Do you replicate the bug in the new system to maintain consistency? Or do you fix it?
There is no clean answer. Replicating the bug feels deeply unsatisfying. Every professional instinct says to fix it now that you have found it. But fixing it means your “like-for-like” migration is no longer like-for-like, and every future reconciliation has to account for the correction. The reconciliation work, which is already the hardest part of any migration, becomes significantly harder when you are trying to separate platform differences from methodology changes.
My view is that, in general, replication first, correction later, is the lesser evil. Get the new system producing the same numbers as the old one, including the known (or newly identified) bugs, and then fix the bugs as a subsequent, documented change with its own impact assessment. It is slower. It is unsatisfying. But it is the approach that keeps the audit trail clean and the actuarial team’s confidence intact.
This only works if errors are logged rigorously at the point of discovery. Each one needs a description of the issue, the products and tranches affected, an estimate of the financial impact, and enough technical detail that a reader coming to the register months later can find commonality across findings and know where to start when the correction work begins. A vague note that says “lapse rates may be slightly off for product X” is not useful. A note that says “the lapse decrement for product X, tranches written between 2003 and 2007, is applied before the surrender decrement rather than simultaneously, resulting in an estimated overstatement of reserves of approximately R2m” gives the next person something to work with.
One big model or many small ones?
A migration forces a structural question that is easy to defer and hard to answer well: should the new platform use a single large model with flags and indicators to handle product variation, or multiple smaller models that are individually simpler but collectively harder to manage?
The single-model approach is appealing because it eliminates duplication. Core logic like decrements, lapse rules, and economic scenario generation lives in one place. Change it once, and every product picks up the change. But the model becomes increasingly complex, the flag-and-indicator logic becomes its own source of bugs, and a change intended for one product can have unintended consequences for others. Testing a single change requires running everything, which in a large model can mean hours of runtime.
The multi-model approach is appealing because each model is smaller, easier to understand, and faster to run. But it introduces duplication, and duplicated logic mutates over time the way DNA does: small transcription errors accumulate with each copy, and eventually two models that should be applying the same lapse basis are not. One gets updated, the other does not. A fix is applied in three of the five copies but missed in the other two. Maintaining consistency across a library of models requires discipline and tooling that is easy to underestimate.
There is no universally right answer. But the choice should be made deliberately at the start of a migration, not allowed to emerge organically, because reversing it later is extremely expensive.
Living with two systems
A related pragmatic question: does every product need to migrate?
For a mature life insurer, the long tail of the book often includes products that are tiny, declining, and odd. They might represent a few hundred policies with unique features that would take weeks to replicate in the new platform. The cost of migration far exceeds the operational benefit, especially if the product will run off within a few years anyway.
The temptation is to leave these in the old system and migrate everything else. This can work, but it introduces its own pain. Running two modelling environments means maintaining two sets of assumptions, two data feeds, two run processes. If the old system is not integrated into the new orchestration and workflow environment, the residual products become a manual process that someone has to remember to kick off, reconcile, and consolidate. Automated kick-off of external models outside the main controller environment is possible in principle, but brittle in practice: it adds integration points, error handling, and monitoring requirements that erode the simplicity gains of leaving the products behind.
Over time, the old system becomes an unloved orphan: patched reluctantly, understood by fewer and fewer people, and increasingly fragile.
The decision should be explicit and time-bound. If the plan is to leave products in the old system, there should be a clear runoff date after which those products will either be migrated or, if they are small enough, approximated in the new system. An open-ended commitment to maintain two platforms in parallel is a commitment that tends to last much longer than anyone intended.
The case for migrating anyway
Given all of this, why would anyone migrate? Because the reasons to move are real, even if the process is painful.
The strongest argument is rarely about the actuarial code itself. The core projection logic, the bit that actually calculates reserves and capital, is broadly the same complexity regardless of the platform. The arithmetic is not all that special. The case for migration is about everything around that core: better long-term vendor support, modern workflow and automation capabilities, improved system architecture that enables genuine runtime improvements, proper change control and audit trails, user management, cloud processing, and the ability to integrate with modern data pipelines.
A legacy model might produce perfectly good numbers, but if the assumption update process involves manually editing text files, if change control is a folder of dated ZIP archives, if running a stress scenario means waiting overnight for a batch job, then the platform is constraining the business even if the arithmetic is fine.
Migration also presents an opportunity, if managed carefully, to clean up decades of accumulated inconsistencies. Not the load-bearing hacks, which exist for good reason, but the other kind: the copy-pasted subroutine that was modified slightly for each product and now exists in seven inconsistent versions. The assumption tables that use three different date conventions because they were built by three different people over fifteen years. The configuration settings that nobody is sure are still active. A migration done with eyes open can rationalise these, as long as the team resists the temptation to rationalise everything at once.
What not to do
One recurring temptation is to skip the vendor platforms entirely and build a bespoke system from the ground up in Python, or Julia, or whatever the language of the moment happens to be. The argument is appealing: we know our business better than any vendor, modern languages are fast enough, and we will have complete control.
I am sympathetic to this, because I have done it. In the early 2000s, I built a proof-of-concept actuarial system on an open-source stack: PHP and C++ on the back end, HTML and JavaScript on the front end, with memoized calculation so that values were computed on demand and cached for reuse. It was designed from the ground up to be standards-compliant so the server could run on a laptop, on premises, or in the nascent cloud. It worked – I’m still impressed with young me there. The core system logic was not the hard part.
The hard part was everything else. The assumptions manager. The data input validation and transformation. Change control and version history. User management and access permissions. Audit logging. Batch processing and job scheduling. Error handling and recovery. Reporting and output formatting. The ability to offload computation to remote infrastructure. That was where I bailed on the project.
Building a basic actuarial projection engine is a manageable problem. A competent developer can get a working prototype running in weeks (it took me a little longer…). But a projection engine is perhaps 20% of what an actuarial modelling platform needs to be. The other 80% is the engineering infrastructure that makes it usable, auditable, and safe in a production environment with multiple users, regulatory oversight, and real money at stake. That 80% is what the established vendor platforms have spent decades building, and it is what a bespoke system will spend years rediscovering.
The comparison is often made to a spreadsheet. Excel’s calculation engine is straightforward. What makes Excel a product is everything around it: the interface, the formatting, the collaboration features, the ecosystem. Building “Excel but for our specific needs” sounds efficient until you realise how much of the value is in the parts you thought were trivial.
A pragmatic framework
If you are considering a migration, here is what I would suggest.
- Be honest about the motivation. If the primary driver is “the code is complex,” that is not a good enough reason. The new code will be complex too, eventually, because the problem domain is complex. If the driver is “the vendor’s support is no longer working” ,” or “our current architecture cannot support the automation and workflow improvements the business needs,” those are real reasons that justify real investment.
- Plan for the migration to take longer than you expect. (Hofstadter’s Law applies with full force here: it always takes longer than you expect, even when you take into account Hofstadter’s Law). Every model migration I’ve seen that stayed on its original timeline did so by cutting scope, not by being faster than expected. Budget for the edge cases, the discovered bugs, the reconciliation work, and the institutional knowledge that only surfaces when someone tries to replicate it. If your initial estimate is eighteen months, plan for three years and you might finish in two and a half.
- Decide your replication and error-handling policy upfront. I have argued for replicating first and correcting later, with a rigorous error log that gives subsequent teams enough detail to act on. Either way, what matters is that the policy is deliberate and that discoveries are recorded with care. Not having a policy is not an option.
- Invest in attribution analysis rather than pass/fail testing. You need to understand why results differ, not just detect that they differ. A 0.1% difference with a clear explanation traced to a specific implementation choice is better than a 0.0% difference that you achieved by accident.
- Make deliberate structural choices early. One model or many? Which products migrate and which stay behind, and for how long? These decisions shape the entire project and are expensive to reverse.
- Resist the temptation to improve the model and migrate it simultaneously. The migration alone is a large enough project. Bundle it with methodology changes and you lose the ability to diagnose whether a difference in output comes from the new platform or the new approach. Migrate first. Improve second. The discipline to separate these is the single biggest predictor of success. (One exception here might be preparing for GPU acceleration or improved vectorisation etc.)
Joel Spolsky was right that rewrites are dangerous. Sonos learned it the hard way. Netscape learned it before them. But Spolsky was writing about software companies, where the old product is still shipping and generating revenue while the new one is being built. Actuarial model migrations are a different beast: you cannot run two production models indefinitely, the regulatory environment is evolving whether you migrate or not, and eventually the cost of not migrating exceeds the cost of migrating.
The trick is to go in with realistic expectations about how hard it will be, a clear-eyed understanding of what you are actually gaining (hint: it is not cleaner code), and enough humility to respect the load-bearing hacks you will encounter along the way. That comment that says “whatever you do, do not remove this”? Read it as a colleague speaking to you across time. They learned something the hard way. Your job is to learn it without repeating the pain.









Leave a Reply