Writing

Modernising a trading platform without stopping the desk

Big-bang rewrites of front-office systems rarely end well. Here is how I approach changing a legacy platform while traders keep using it every day.

Every trading platform I’ve worked on has had a part of the codebase that nobody wanted to touch. Usually it was the part that made the most money. It had grown over ten or fifteen years, it had survived several reorganisations, and the people who understood it best had long since moved on.

At some point someone senior suggests rewriting it. I understand the appeal. But in my experience a full rewrite of a front-office system is one of the riskiest things a technology team can take on. The old system keeps changing while the new one is being built, the business can’t pause while you catch up, and the cutover weekend becomes a single point of failure for months of work.

There is a slower, less dramatic route that works far more often. It goes something like this.

Start with what the system actually does

Before changing anything, I want a plain written account of what happens on the platform during a normal day. When do prices arrive? When does end of day run, and what depends on it? Which reports do the operations team check before they go home, and what do they do when the numbers look wrong?

This sounds like documentation for its own sake, but it pays for itself quickly. On more than one platform, writing down the chronology of the daily processes has turned up jobs that nobody knew were still running, and dependencies that would have broken a migration in ways no test would have caught.

It also gives the business a shared picture. Conversations with finance, compliance and the desk get much easier when everyone is pointing at the same diagram.

Make it safe to change before you change it

Legacy code is hard to modify mostly because it is hard to test. Large classes create their own dependencies, talk directly to the database, and read configuration from wherever happened to be convenient at the time.

So the first engineering work is usually unglamorous: introduce dependency injection, put seams around the database and external services, and get a meaningful set of tests in place around the behaviour that matters. On one large monolith we added attribute-based registration so that existing classes could opt into the container one at a time. Nothing about the system’s behaviour changed, but suddenly we could write tests for it.

Structured logging and proper observability belong in this phase too. If you can’t see what the system is doing in production, you can’t tell whether your changes have made it better or worse.

Grow the new design inside the old one

Once there is a safety net, I prefer to build new capability as a cleanly structured module inside or alongside the existing system, rather than in a separate greenfield project. The new code follows Clean Architecture and a properly modelled domain. The old code carries on as before.

The important piece is the boundary between them. The new module should never take on the old system’s data structures or naming. A thin translation layer keeps the two worlds apart:

public sealed class LegacyTradeTranslator : ITradeSource
{
    private readonly ILegacyTradeRepository _legacy;

    public LegacyTradeTranslator(ILegacyTradeRepository legacy) => _legacy = legacy;

    public async Task<Trade?> GetAsync(TradeId id, CancellationToken ct)
    {
        var row = await _legacy.FindByRefAsync(id.Value, ct);
        if (row is null) return null;

        return new Trade(
            new TradeId(row.TRADE_REF),
            Instrument.FromLegacyCode(row.INSTR_CD),
            Money.Of(row.NOTIONAL_AMT, row.CCY),
            row.TRADE_DT);
    }
}

It looks trivial, and that’s the point. Every odd legacy convention gets handled in one place. When the old data store is eventually replaced, this class changes and the domain code doesn’t.

Move one capability at a time

With a clean module in place, functionality can move across piece by piece. Pick something with clear edges and real value to users: a new view, a reporting job, an event that other services need. Ship it, let people use it, and learn from it before taking on the next one.

Messaging helps a great deal here. If the legacy system publishes events when trades are booked or amended, new services can react to them without anyone having to touch the old code. Over time more and more of the platform’s behaviour lives in the new world, and the legacy core gets smaller and quieter.

Make every release a little safer than the last. Retiring the old system will follow.

Keep the people with you

Most of the risk in these programmes isn’t technical. Traders need to trust that the numbers are right. Operations need to know what to do when something fails at 6am. Senior stakeholders need to see steady progress, not a long silence followed by a launch.

A few habits make a big difference:

  • Release small and often, so that each change is easy to explain and easy to roll back.
  • Write runbooks as you go, not at the end. The people supporting the system should never be surprised by it.
  • Report in business terms. “End-of-day risk is now ready forty minutes earlier” lands better than “we migrated the batch service”.
  • Bring the existing team along. They know where the bodies are buried, and they’ll be running the system long after any consultant has left.

Is it slower?

On paper, yes. The incremental route rarely produces a dramatic before-and-after slide. But it delivers value from the first month, it never puts the desk at risk, and it doesn’t depend on everything going right on one weekend. In financial services that trade-off is almost always worth making.

If you’re weighing up a rewrite of a platform your business depends on, I’m always happy to talk it through.