Why Expensive Software Development Never Looks Expensive

I specialize in developing object-oriented java applications that aligns with business objectives, using Domain-Driven Design principles to ensure technical decisions drive tangible value. By focussing on a deep understanding of the business domain, I craft solutions that solve real problems while maximizing ROI. My approach evaluates the cost/profit ratio of every decision—only implementing technologies when the benefits outweigh the costs. I’ve been called in to revive stalled projects and address challenges where others have struggled. My focus is on creating software that not only meets but exceeds business expectations. Whether working with legacy systems or modern frameworks, I select the right technologies to maximize value—not just follow trends. I believe software should be a strategic asset, and this mindset guides every decision I make in development.
Every organisation that has run a significant software system for more than a few years has felt a version of the same thing: a change that should have taken days takes months, nobody can quite explain why, and the explanation that eventually gets offered — the domain is complex, the requirements changed, the previous team was careless — is almost never checked against an alternative approach for the software architecture or alternative framework choices, because the alternative was never built. There is no possible comparison to determine the solution chosen is a good one and there is no benchmark to measure "fit for purpose."
This is the unfalsifiability problem, and it is worth stating plainly before anything else in this piece, because it is the reason the cost described below is so rarely traced back to its actual cause. Every system is built once. There is no version of your platform built the other way, running alongside it, that anyone can compare it to. So when a system works, the approach that produced it gets read as validated. When a system becomes expensive to change, the cost gets attributed to anything except the structural decision that caused it — because that decision was made years ago, by people who may have moved on, and there is no control group to prove that the structure was the variable that mattered.
That absence of a control group is not a minor academic point. It is the reason a specific, avoidable pattern of cost has been able to spread through the industry for decades, get taught in courses, get validated in interviews, and still never be clearly named as a mistake. This article is an attempt to name it — and to offer something more useful than a diagnosis: a way to check, this week, whether it applies to you.
The Villain: Process Over Product
Ask almost any team building a significant piece of software what the goal of the project is, and the honest answer, more often than anyone would like to admit, is not "build the best-fitting product for this business." It is "follow the process correctly." Those two goals overlap often enough that the difference goes unnoticed for long stretches — right up until the moment they diverge, at which point one of them has to be compromised, and it is almost always the product that loses, because the process is what gets measured.
The clearest place to see this is the sprint. A sprint is regularly treated as something close to a small contract: a ticket is opened, work begins, and the implicit goal becomes finishing what was started, as written, inside the sprint boundary. But a sprint was never supposed to be a contract. It is a checkpoint — a scheduled moment to ask whether the team is still building the right thing, not a deadline that obligates them to keep building whatever they said they would two weeks ago, even after it has become clear the ticket was wrong. When a team discovers, mid-sprint, that a task was scoped incorrectly or is solving the wrong problem, the question that should get asked is simply "are we building this correctly," not "how do we still close this ticket by Friday." Closing the ticket is a process goal. Building the right thing is a product goal. Optimising for the first at the expense of the second is a choice, made quietly, dozens of times a sprint, that nobody frames as a choice at all.
Test coverage carries the same tension in a different form, though it deserves a careful statement, because tests are genuinely valuable — for preventing regressions, for guaranteeing behaviour that actually matters, for catching what a reader would miss in logic that is legitimately intricate. The problem is not testing. It is treating a coverage number as the goal itself, rather than as a tool applied where the underlying risk actually warrants it. "Write tests for everything" is a policy, and once it is a policy, coverage becomes something to satisfy rather than something to reason about — the same substitution as closing the ticket standing in for building the right thing. The actual question is proportional, not universal: how much does this particular piece of code need a test, given how easy it already is to read and predict? Code whose structure makes its purpose explicit lets the next developer catch a functional gap by understanding it, the same way a domain expert notices a wrong assumption by reading a model rather than by waiting for something to fail. Code that requires a large surrounding suite just to reveal what it is supposed to do in a given case is not made safer by that suite — the suite has become the place the missing legibility went to hide. Tests earn their cost against genuinely error-prone or consequential logic. Applied as a blanket mandate regardless of what is actually being tested, they are a process metric standing in for a product judgment.
Neither example is an argument against sprints or against tests. Both are examples of the same underlying pattern: a process artefact, adopted for a good reason, quietly becoming the thing being optimised for, while the product — the thing the process was supposed to be in service of — becomes secondary, and nobody notices the moment it happened.
Why This Produces the Same Tools, Regardless of Fit
Process-over-product does not announce itself as a mistake. It shows up as a default: a framework, an architectural style, a way of organising a codebase, chosen not because anyone evaluated whether it fit the problem, but because it is what the process expects, what the team already knows, or what the last project used.
It is worth being honest about why patterns like heavy service layering, event-driven architecture, or a framework's particular conventions get chosen this often, because the honest answer is not stupidity — it is a genuine, defensible fit for a specific kind of team. When developers rotate frequently, when work is deliberately broken into small, tightly specified tickets, when the organisation is optimising for the ability of any developer to pick up any task with minimal ramp-up, a rigid, uniform, low-context structure is a reasonable trade. It sacrifices peak comprehension of the system as a whole in exchange for consistency and a short on-ramp for whoever picks up the next ticket. That is a legitimate choice, for that context — the same logic that makes an assembly line sensible for certain kinds of manufacturing.
The failure is not using that structure. It is using it by default, applied to a context where the actual constraint is the opposite one — a long-lived, complex, low-turnover core system, where the thing that matters most is not how quickly an unfamiliar developer can complete an isolated task, but how deeply the team's understanding of the business is reflected in the code, and how cheaply that understanding can be corrected when it turns out to be wrong. Applying an interchangeability-optimised structure to a comprehension-optimised problem is a mismatch, and the mismatch is invisible on the day the choice is made, because both approaches compile, ship, and pass their tests. It only becomes visible years later, in the cost of the change that should have been simple.
Take an ordinary example. A tax rate changes — in principle, a one-line update: a single number, changed in one place. In a system where that calculation exists once, as a single rule a finance person would recognise, it usually is exactly that: one change, applied everywhere it needs to apply, because there is only one place it could live. In a system where the same calculation was implemented separately, over several years, by whichever part of the system happened to need it at the time — order processing, invoicing, refunds, reporting — the same change now means finding every place the rate was written down, confirming none of them quietly diverged from the others somewhere along the way, and testing each one independently, because nothing in the system ever enforced that they were the same rule to begin with. A one-day change becomes a three-week project. Nothing about the system is broken. Every part of it still works. The cost was always there — it was simply invisible until something forced it into view.
The Bigger the System, the Simpler the Code Needs to Be
This mismatch does not cost a fixed amount. It scales — directly, and in the wrong direction — with the size and longevity of the system it is applied to.
A human mind can hold a limited amount of context at once. A small system fits inside that limit regardless of how it is structured, which is why a mismatched architecture on a small application rarely causes visible pain — there simply isn't enough complexity yet for the mismatch to matter. As a system grows, that stops being true. The team's collective ability to hold the whole system in mind does not grow at the same rate the system does, and at some point the only thing that keeps the system navigable is whether its structure makes the business explicit — whether a developer can look at the code and see what the business actually is, rather than having to reconstruct that understanding from services, layers, and event subscriptions scattered across the codebase.
This is precisely backwards from how complexity tends to get handled in practice by the process/framework driven approach. The more complex an application becomes, the more elaborate its supporting structure tends to get — more services, more layers, more indirection, more process — on the theory that complexity should be met with more machinery. But machinery is not what makes a large system comprehensible. Explicitness is. The bigger and more consequential the system, the simpler and more direct the code needs to be, because simplicity is what makes the essential complexity of the business — the actual rules, relationships, and responsibilities — visible to a mind that cannot hold the whole system in memory at once. A system that makes its structure explicit tells a developer when something is wrong. A system that has dissolved its logic into layers and services waits for a customer, an auditor, or a very expensive incident to tell you instead.
What Actually Controls the Lifetime Cost of a System
There is a second, related cost that rarely gets priced in at the point of decision: the lifespan of the framework and the architecture the system is built on is not the same as the lifespan the business needs from the system itself.
Frameworks version, deprecate, and eventually stop receiving free support. Distributed architectures accumulate their own maintenance burden — contracts between services that need to be kept synchronised, infrastructure that needs to be operated, failure modes that need to be observed and traced across boundaries that a single deployable unit would never have needed in the first place. None of this is a one-time cost. It recurs, every year, for as long as the system runs, and it is paid regardless of whether the business logic underneath it has changed at all.
The degree to which this matters depends entirely on how entangled the business logic is with the framework and the distribution model sitting underneath it. When the two are kept genuinely separate — when the logic that represents what the business actually does does not depend on a particular framework's conventions or a particular network topology to be correct — the framework and the distribution model become swappable. A major version upgrade, a framework migration, even a move away from a distributed architecture back to a simpler one, becomes an engineering project with a bounded cost, rather than a rewrite. When the two are entangled — when the business logic is expressed in terms of the framework's annotations, or depends on a particular sequence of asynchronous events across services to behave correctly — every upgrade, every migration, every framework decision made by a vendor becomes a forced decision for the business, on someone else's timeline, at a cost the business had no say in setting.
Independence from framework and from physical distribution is not a purity concern. It is the single largest control most organisations have over the lifetime cost of a system, and it is decided almost entirely at the point the system is first structured — long before anyone is thinking about framework versions or infrastructure bills.
A Simple Test
None of the above is abstract. It can be checked, directly, against a system you already have, in a few minutes. Ask:
Can a new developer read the core logic and understand what the business actually does directly from the code — or does understanding it depend on documentation, a wiki page, or someone explaining it out loud? If non-code documentation is required to understand what the system does, that is itself a signal that the code is not legible.
If your current framework disappeared tomorrow, how much of your actual business logic would survive untouched?
When a rule needs to apply in two different places, does it genuinely live in one place — or has someone had to remember to update it twice?
Are your service or module boundaries a reflection of the business, or a reflection of how the org chart, the deployment pipeline, or last year's team structure happened to be drawn?
A "no" to any of these is technical debt — not a crisis on its own, but a specific, named cost that is now accumulating quietly, whether or not anyone is tracking it. Most systems carry some of this, and carrying a little is rarely fatal. What is worth paying attention to is how many of these land on "no," and how long they have been accumulating — because that is what predicts whether the next significant change costs what it should, or costs considerably more.
It is worth being honest about the limits of asking these questions yourself, or having them answered by someone already inside the team — and it is worth being equally honest that "independent" is not, by itself, enough. A review by someone who is simply outside the team but fluent in the same framework and the same conventions will still tend to evaluate the system from inside those conventions: a Spring application reviewed by a Spring expert will get a great deal of useful feedback on how well Spring was used, and very little on whether Spring, or the architecture built around it, was the right choice for this problem in the first place. That is not a competence problem. It is a vantage point problem, and a shared vantage point does not get corrected by adding a second person who shares it.
What is actually useful here is a genuinely contrarian review — one that starts from the domain rather than from the conventions already in place, and is explicitly looking for reasons the current choices might be wrong, not confirmation that they were applied correctly. The goal of an engagement like that is not to produce advice you are obligated to adopt. It is to generate as many concrete remarks, objections, and alternative approaches as possible, so that even the recommendations you choose not to act on have at least been seen from outside the room they were originally decided in. A focused engagement — a few days in the codebase and with the team, examining the core domain, the framework's boundaries, and where the two are entangled — is enough to produce a written report naming the specific structural decisions currently driving cost, in terms a budget owner can act on, along with concrete, sometimes contrarian, recommendations for what to do about it.
If the test above raised more questions than it answered, that is usually the point at which a second, independent set of eyes is worth more than another sprint of guessing.





