DEV Community

Lukasz Tarczyluk
Lukasz Tarczyluk

Posted on

When You Inherit Software That Can No Longer Be Built

There is a particular kind of silence that falls over a room when you ask a simple question and nobody can answer it.

The question was: how do we build this?

The system in front of us was a set of pricing applications at a large European bank — the components that answer "what is this instrument worth today" for the front office. They consumed live market data from Reuters feeds, applied the bank's own valuation logic, and served the result to booking and settlement systems downstream. They had been running for over a decade and, by every operational measure, were working fine — restarted once a week, otherwise left alone.

And nobody could build them.

The developers who had written them had left the organisation years earlier. What remained was a deployed artefact — one image, built at some point in the past by someone who understood how all the pieces fitted together, running unchanged ever since. The source code existed. The build did not. No one still employed knew how to assemble a deployable package from that source together with its full set of dependent technologies.

If you have never encountered this, it sounds like negligence. It isn't. It's entropy. It happens slowly, to well-run organisations, and by the time anyone notices, the people who could have explained it are three jobs away.

What "unbuildable" actually costs

A frozen system that works is easy to rationalise. It works. Leave it alone.

The cost is invisible until you need something.

A defect appears. You cannot fix it — you can only work around it in the systems downstream. A dependency turns out to have a published vulnerability. You cannot upgrade it; you compensate at the network layer and write a risk acceptance instead. A regulation changes, and this is finance, so regulations change. You cannot implement the change where it belongs.

And underneath all of it the runtime itself is ageing out. The Java version eventually loses support, and with it your ability to run on infrastructure anyone is willing to maintain.

Every one of these turns into a workaround somewhere else, and each workaround makes the next change harder. The frozen component quietly exports its rigidity to everything around it.

So the first deliverable of the project was not a feature. It was the recovery of the ability to change the software at all.

The archaeology phase

Before proposing a target, we had to understand what was actually there.

Archaeology is the right word, because there was nothing else to go on. No internal documentation had been left behind — no architecture notes, no runbook, nothing explaining why any of it was shaped the way it was.

External help was thin too. Oracle's OCEP was never widely adopted; the vendor documentation is sparse and the community around it is small enough that searching an error message often returns nothing at all.

The product did ship a visual editor that rendered the event-processing flows — the one artefact that would have explained the transformations at a glance. It ran in a browser, and it required Flash, which had been end-of-life for years. On a bank-issued machine you install nothing that isn't on the security-approved software list, and an obsolete browser plugin was never going to be on it. The tool that would have explained the system existed, and was unreachable.

So I read the XML instead. Line by line, file by file, reconstructing what each transformation did and how the stages fed one another, until the shape of the system existed somewhere other than inside a program nobody could run anymore.

That is the unglamorous centre of most legacy work. Not clever refactoring — patiently rebuilding an understanding that was never written down in the first place.

Two things dominated the architecture that emerged from this:

OSGi, inside the applications. Each pricing application was built as an OSGi bundle system — decomposed into modules with dynamic loading, versioned interfaces, and a service registry.

A separate event-processing server, outside them. For event handling, the applications published events to a dedicated Oracle Complex Event Processing (OCEP) server that ran alongside them, with its rules and streams configured outside the application code.

Both are legitimate technologies, chosen by capable people for real reasons. In this system, though, both had become expensive — in operational complexity, in the amount of context a developer must hold before making a safe change, and in the number of ways a deployment can fail.

The question I kept asking was not "how do we migrate this" but "what requirement made this necessary?"

For OSGi: the promise is hot-swapping modules at runtime without restarting the process, and running multiple versions of a dependency side by side. Did these applications need that? They went down on a schedule every week regardless, and nobody had hot-deployed a bundle in years. The dynamic module model was carrying no weight.

For OCEP: the promise is high-throughput stream processing with complex temporal rule matching over continuous event streams. What was it actually doing? Routing a modest volume of events between the applications — work that belongs in a few lines of in-process code, not in a dedicated server standing alongside them.

This is the uncomfortable finding that a legacy migration often surfaces: the complexity is not load-bearing. It was a reasonable bet by capable engineers under different assumptions, and those assumptions stopped being true years ago. Nobody re-examined it, because the system worked, and because re-examining it required exactly the build capability that had been lost.

Choosing boring on purpose

The target architecture was deliberately unambitious: rebuild each application as an ordinary Spring Boot service on a current Java LTS — dropping the OSGi bundle model inside it, and replacing the external OCEP server it published to with plain in-process publish/subscribe (Guava's EventBus, at the time), which covered the entire real requirement in a fraction of the surface area.

This is the part that tends to attract argument, because the industry reflex runs the other way — towards shared platforms and extracted infrastructure. But nothing here was being shared to any purpose: both pieces were an entire extra tier to run, secure, patch and understand, in exchange for capability the applications did not use. Taking it away and letting each stand on its own removed cost, not capability.

The decisive criterion, though, was the one we started with: any Java developer must be able to build and release this. That single requirement eliminated every option that preserved the old packaging. A conventional Maven build producing a conventional Spring Boot artefact is not an interesting technical choice. It is an interesting organisational one, because it means the system's future no longer depends on knowledge held by specific people.

Boring technology is legible technology. Legible technology survives staff turnover.

The constraint that shaped everything: don't move the contract

While this was underway, the bank went through a corporate integration and several internal upstream systems were replaced with the acquirer's equivalents. The market data feed itself was unaffected — what changed was the systems sitting behind it. New connectors were needed.

And here was the hard constraint: a substantial number of legacy systems consumed these applications, some against older versions, and they had to keep receiving byte-for-byte identical responses. We were changing what was behind the interface. We were not permitted to change the interface — not the shape, not the field ordering, not the numeric formatting, not the rounding.

That last one matters more than it sounds. Financial output is full of details that a rewrite quietly "improves": a trailing zero, a scale on a decimal, a null rendered as an empty string. Every one of those is a production incident in a downstream system you have never heard of.

The approach was straightforward but disciplined:

  1. Map the new sources back onto the existing response model rather than designing a better one.
  2. Capture real production responses from the running system as a reference corpus.
  3. Verify equivalence of the new implementation against that corpus before cut-over — not "looks right", but identical output for identical input.

Recorded production traffic is the only honest specification a legacy system has. The documentation describes intent. The code describes mechanism. Only the observed output describes what downstream consumers actually depend on — including the behaviours nobody meant to promise.

Closing the loop

The last piece was the one that made the rest durable: a CI/CD pipeline that builds from source, runs the tests, and promotes through environments onto a container platform. Ordinary, in other words — which was the whole point.

The before-and-after is the clearest measure of the project.

Before: an artefact nobody could rebuild, left untouched for years, with every change request answered by a workaround somewhere else.

After: any Java developer on the team can build and release a new version in minutes.

No latency numbers, no throughput graphs. The system priced instruments the same way on the last day as on the first. What changed is that it became possible to change it — and that turned out to be the capability the organisation had actually lost.

What I'd take from it

  • "It works" is not the same as "it's fine." A system you cannot rebuild is a system with an expiry date; you just don't know the date.
  • Ask what requirement justified the complexity. Often the requirement is gone and only the complexity remains. Sophisticated technology chosen for reasons that no longer apply is just cost.
  • Optimise for who can operate it, not for elegance. If only one person can build it, you don't have software — you have a dependency on that person.
  • Recorded output is the real contract. Not the docs, not the schema. Behaviour that consumers depend on is behaviour you promised, whether you meant to or not.
  • Restoring changeability is legitimate work. It doesn't demo well and it has no user-visible feature attached, but nothing else you want to do is possible until it's done.

The most valuable thing we delivered was not a rewrite. It was ending the silence that follows how do we build this?

Top comments (0)