What fifteen years of maintenance taught us about design
Fifteen years in, the systems we still maintain have taught us more than the ones we launched. Some early decisions aged quietly well. Others cost us for a decade.
Engineering practice7 min read
We have been building this kind of software since 2010. What the case studies leave out is everything after the launch: the change requests, the framework upgrades, the morning somebody asks whether one field can be made optional and it takes four people two days to work out what depends on it. Fifteen years of that has been a better design education than any of the go-lives.
A design decision has a half-life. Screen layouts go stale in about three years and nobody suffers much, because replacing a screen is contained work. Data model decisions do not go stale. They compound, because every report, integration and downstream consumer built afterwards assumes them. What follows are the ones we can now see clearly at either end: a few that quietly saved a great deal of money, and a few we paid for over a decade.
One record, even when splitting it was easier
The CRM we built for Würth carries roughly thirty thousand field representatives. Leads, visits, pipeline and timesheets had each lived in a different third-party product, and the obvious plan was to integrate four systems rather than replace them. We put all four into one data model instead.
The reason had nothing to do with tidiness. Every question worth asking crosses those boundaries. Which visits turned into pipeline. Whether the route a representative drove matches the calls they logged. Where a region's time actually goes. If those facts live in separate products, the answer comes from a nightly reconciliation job, and reconciliation jobs are where systems go to rot. They fail quietly, they drift, and the first person to notice is usually a manager who has stopped trusting the report and started keeping a spreadsheet.
Put the join in the schema. It is more work in month two and it is the difference between a query and a project in year six.
An integration surface that arrived before the channels did
The booking platform we built for WTI Cabs runs on an API-first core: one inventory, served to the web application, the mobile application and partner channels through the same interface. Fourteen cities and something over two and a half thousand vehicles sit behind it.
At the time this was an argument, and not a short one. Building the interface first is slower than building the website, and the website is the part anybody in a steering meeting can see. What it bought was that each channel added afterwards was configuration rather than a piece of work, and that the team had to agree on what a booking, a vehicle and a city actually were before writing code that assumed three different answers. A shared vocabulary enforced by a compiler is worth more than a glossary in a document nobody opens.
Language and locale treated as structure
For Zain Bank we shipped native Android and iOS clients in Arabic and English against a shared services layer. For Razain we built a travel platform serving right-to-left text and multiple currencies across the Middle East. Both settled locale in the first week of design.
The timing is the whole point. Retrofitting bidirectional text is not a translation exercise. It reaches layout, iconography, input validation, sort order, date handling and every screen with a directional affordance on it. Decided at design time it costs a few weeks. Decided in year two it costs the interface, and somebody senior has to explain why a request that sounded cosmetic became a rebuild.
Configuration used as a substitute for a decision
Now the expensive habits. The one that cost us most was making things configurable whenever two stakeholders disagreed. A flag is cheap to add and it ends the meeting. Fifty flags later there is no such thing as the system, only the particular combination running in one environment, and no test suite covers the combinations that matter because nobody has a list of which ones exist.
We have inherited systems of our own where the honest answer to what does this do in production required reading the database. That is not a proud sentence to write about work with our own name on it, and we have written it more than once.
A settings page is a decision deferred at compound interest.
The disagreement now goes back to whoever owns the process, and the answer gets recorded. Where a setting genuinely varies by tenant or by jurisdiction it stays, with a named owner and a test that exercises both branches. Everything else gets decided once, by a person, with their name on it.
A shared library across clients who had never met
At one point we built an internal platform library so several engagements could share the same authentication, reporting and workflow components. On paper that is leverage: fix a defect once, every account benefits.
In practice each client needed a variation. Each variation added a branch, then a version pin, then an exception. Within two years, upgrading the library for one client required agreement from three others who had no contractual relationship with each other and no reason to care about anyone else's roadmap. Delivery plans that should have been independent were coupled through a repository none of them had bought.
We stopped. Shared engineering standards travel between accounts well: the same code review process, the same testing standard, the same release procedure in both offices. Shared code across contract boundaries does not travel, unless it is small, stable and genuinely generic. Copying is cheaper than coupling more often than engineers like to admit.
Integration priced as contingency
For several years we estimated integration as a percentage of the build, which is roughly the same as estimating it by mood. Connecting a new system to the estate around it is usually the largest single item in an enterprise programme, and it is the item least under our control. Every counterparty has an owner, a change process, a release calendar and a holiday season.
It now gets scoped explicitly, before contract: which systems, which interfaces, who owns each one, when they can test with us, and what happens when they cannot. The number is larger and it is defensible. Bids where a client compares it against a competitor who buried the same work in contingency are bids we sometimes lose. We have made our peace with that, mostly.
Access models bolted on afterwards
Twice we have added multi-tier access to a system designed for one class of user. Both times it touched every query in the application, because a permission model that arrives late becomes a filter applied everywhere rather than a shape the data already has. On the Würth build, regional visibility was in the model from the start, so a manager seeing only their own territory is a property of the query rather than a check bolted on after it.
The rule we take from that is short. Identity, tenancy and permission belong in the first schema, even when the first release has one tenant and everybody in the room is an administrator.
Reversibility is the thing worth designing for
The pattern underneath every item above is how expensive a decision is to reverse. Cheap to reverse: screens, wording, report layouts, most of the visual system. Make those quickly, get some of them wrong, fix them on a Thursday. Expensive to reverse: the data model, the identity and permission model, the integration contracts, the locale strategy, and anything a third party has already started depending on.
Design time in most projects is not distributed across those two lists in proportion to the risk. It is distributed according to what stakeholders can see in a review meeting, which is very nearly the inverse. Half an hour goes on a button and the tenancy model gets waved through, because one of them is on the screen and the other is in a diagram.
The lesson we were slowest to learn
For years we treated continuity as a documentation problem. Every engagement ended with a substantial handover pack, and the packs were good, and clients signed them off. A document only helps somebody who already knows what to ask. The engineer arriving in year three knows that a job failed. They do not know the job has three siblings, that two are safe to rerun and one is not, and that the difference is a flag in a table nobody mentions because it is not architecture.
What changed the outcome was rehearsal in place of writing. An unfamiliar engineer follows the procedure while its author sits there and says nothing. Whatever breaks in that hour is the documentation that was actually needed, and we write that and skip the rest. It is slower, it is mildly humiliating for the author, and it finds things no review ever did.
Fifteen years is long enough to watch a system outlive two or three generations of the people who operate it. That is the test we now design against. On an ordinary Tuesday in year nine, somebody we have never met needs to change how a form validates a tax identifier and get it into production by Friday, without ringing us and without breaking the month-end close. Most of whether they can was settled before anybody wrote a line of code.
More on engineering practice
All writingAugust 2025
Senior engineers in a client's team, and the reporting line
Embedded engineering works when one question is settled in writing: who sets priority. Ambiguity there produces most of the friction people blame on culture or communication.
May 2024
Offline-first is a product decision
Queueing writes is the easy half. What happens when a representative returns from two days without signal is a product question, and it usually gets answered by whoever wrote the sync code.
July 2023
Test automation as a service, and when it stops paying
A test suite is code, with the same rot and the same maintenance cost. Where automation earns its keep, which tests are worth deleting, and how to tell when a suite stopped paying.
Talk to our engineering team
Tell us what you need built, modernised or maintained. We will tell you whether we are the right firm for it and what it costs.