A global website has been in production for seven years. It has been through three major framework versions, two Node migrations and a full redesign of its main templates. The annual budget sets aside around 18% for maintenance, which is a defensible figure and one nobody argues about in a steering committee.
What the interface team lives through is something else. More than half of every iteration goes into wrestling with what is already there: working out which of the five cards has to be touched, checking what breaks when a spacing value moves, repeating in four places the contrast fix that was only applied in one. None of that shows up as maintenance on any board, because the developer who spends eleven days on a carousel that should have cost three books the time against the campaign that asked for it.
There are two costs mixed together there, and they are worth separating, because only one of the two can be paid off.
Debt is what you take on knowingly
The borrowing metaphor is almost always used badly, and it is worth giving it back its narrow meaning. Taking on debt is a decision somebody makes: you borrow time, you ship earlier than you could ship it well, and you pay it back later with interest. The interest is what every later change costs on top while that code stays as it is. At no point was it a licence to write something badly and promise to fix it afterwards.
I use a working definition that rules out almost everything that gets called debt in a meeting. There is debt when three things exist: a creditor, meaning who is going to pay the interest and in which specific piece of work; an amount, how much that interest costs per iteration; and a due date, which release pays it back. If any of the three is missing, what you have is not debt.
A case of real debt, the kind you sign without remorse: a form component is needed that the system does not have, there are five weeks left before a new market opens, and the decision is to build it inside the module that needs it, coupled and without tests, with promotion to the design system booked for the following quarter. It is written down, it has an owner and it has a date. It can be brought forward, deferred or allowed to lapse, but everyone knows it is there.
Debt like that is expensive and it is still the easy problem. It is listed somewhere, it can be estimated, and when it gets paid back is negotiable. A team whose problem is that has a scheduling problem.
The rest was never borrowed, it accumulated
The other cost meets none of the three conditions. Nobody decided it, nobody wrote it down, and there is nothing to pay back because nothing was ever borrowed. I call it decay rather than debt, and the difference is not a matter of vocabulary: debt shows up when a team is in a hurry and does something badly on purpose, and decay shows up when a team delivers well and on time, over and over, on an architecture that was never prepared for what it was going to be asked.
That is worth pausing on, because it runs against intuition. The developer who duplicates the card for the sale campaign is doing the right thing in their context: they have nine days, the shared card is consumed by fourteen views, it is the week before the traffic peak of the year, and touching shared code means a cross-team review with two teams that are not going to make it. Copying is the prudent move. The failure is not in that decision, it is in the fact that the system offered no option that was cheap and correct at the same time. Multiply that by six campaigns a year for seven years and you get forty-two duplications, each one impeccable at the time, adding up to a product where nobody knows how many cards there are any more.
This is what makes the word maintenance mislead whoever approves the budget. It sounds like repairing what has broken, and most of the work filed under that label is new functionality built inside something that was already built. The quality of what was already built is what sets the price of everything that comes after it.
Where it accumulates, in the interface layer
The presentation layer decays faster than the rest of the system, and there is a mechanical reason for it. TypeScript has a compiler that warns you about dead code; a stylesheet does not. Across a catalogue of thousands of templates nobody can delete a selector with any guarantee, so the only safe operation on CSS is to add. The volume grows monotonically for years, and every wave of architecture leaves its own layer behind: the BEM one, the SCSS mixin one, the utility one, the framework-scoped one. All four live together and none of them is ever fully retired.
On top of that, the two problems I see most often carry very different weights and should not be treated the same way.
Not having a token layer of your own is the most expensive to live with and the cheapest to fix. When the brand value is written out literally in three hundred places, any cross-cutting change turns into a find-and-replace with manual visual review. A contrast adjustment to meet accessibility, a dark mode, a per-market brand variant: each one costs work that is linear in the size of the codebase, and it cannot be fully automated because somebody has to decide, case by case, whether that grey was the border grey or the secondary text one.
Domain-coupled components are the opposite: more expensive to fix and slower to degrade. A card that knows what a product is, calls the pricing service and decides its own currency format is no use in checkout, or in the wishlist, or in search results. It gets copied. Three years later there are five that resemble each other and diverge, and every accessibility fix has to be made five times, badly, and by the fifth nobody remembers it existed.
My estimate, and I give it as a field estimate rather than a published figure, is that in a product of this size with no library of its own and no token layer, between 40% and 60% of the front-end team’s time goes into absorbing decay. The top half of that range belongs to products with short, recurring campaign cycles, which are exactly the ones that leave least room to do things any other way.
Assisted generation does not invent decay, it speeds up the decay already there
This is where the arithmetic has changed over the last two years, and not in the direction it is usually told. The term vibe coding, coined by Andrej Karpathy in 2025, describes a way of working where you accept generated code without reading all of it and go by whether the result works. For a throwaway prototype that is reasonable. In the interface layer of a seven-year-old product it has three effects that compound.
The first is that the model writes from the context it can see. If the repository holds five diverging cards, it produces the sixth, and it produces one that looks like whatever repeats most, not like the architecture somebody had in mind. A generator aims to fit in with the existing code, and in a decayed product the existing code is precisely the problem. Every iteration widens the material the next one learns from.
The second is economic, and it is the one most often skipped over in conversations about productivity. Writing a component from scratch used to cost two days, and that friction meant somebody, at some point, asked whether the thing already existed. The question was not being held up by design system governance or by team discipline: it was being held up by cost. Once generating a component costs forty seconds, the only brake on duplication that was actually working disappears, and what remains is a discipline that in most teams was never written down anywhere, because it never needed to be.
The third is in review. A generated component is reviewed against whether it works: tests pass, the screenshot looks right, it goes in. The question that does not get asked is the one about belonging. Whether that component should exist at all, which layer it consumes from, what contract it fulfils, who maintains it when the brand changes. No test and no trained eye answers that, because the answer is not in the code being reviewed, it is in the system as a whole.
The three together give you what you would expect: the volume grows, the context gets worse, the next generation starts from a worse context and produces more volume. The speed of the first iteration is real and it is the one that shows up in the demo. The cost arrives spread across the next forty, in pieces too small for anyone to add up.
What does work is to stop treating architecture as tacit knowledge and turn it into something that can be read. A model works well when the direction is in the context it receives: tokens as a data file rather than an oral convention, lint rules that break the build on a literal hex value inside a component, a file of composition rules at the root of the repository, visual regression tests that catch the new card looking too much like one that was already there. An architecture document in Confluence does not do this job, because the model does not read it and neither does the developer with a nine-day deadline.
It is worth saying where assisted generation is very good, because that is exactly the other cost. Paying down recorded debt is work with a specification: renaming three hundred uses of a token, writing the codemod for a major migration, extracting a coupled component into an agnostic layer following a pattern that already exists. There it performs above what a team managed three years ago. Slowing decay is the opposite: it consists of saying no to code that works, and that is not delegated, not to a machine and not to anyone without authority over the architecture.
Three numbers before deciding anything
Any plan that does not start by measuring is an opinion. There are three figures you can get in an afternoon that are worth more than any consultancy estimate.
How many distinct literal colour values there are in the repository, against how many the brand identity defines. The distance between the two numbers is the size of the token problem, and it usually runs to one or two orders of magnitude.
How many different components paint the same visual piece. You count it by hand across the five pieces that appear most: card, button, form field, modal and list. That is the measure of domain coupling.
How long a change to a single brand value takes to reach production, from approval to being deployed across every market. This is the number a director understands, because it talks about time rather than code, and it is the one that turns an architecture conversation into a business conversation.
What I would do: two speeds on the same team
The way of intervening I argue for splits the work into two tracks running at different speeds. A short track that handles the day to day, the campaigns and whatever is committed to the business. And a long track that builds the new architecture and replaces the old one underneath it, with no migration project and no cut-off date.
This has names, none of them entirely settled. The idea that the layers of a system change at different speeds and should not be handled at the same rhythm is called pace layering. The mechanics of replacing the old from the inside, with no cut-off date, is the strangler fig, which grows around the old tree until it can stand on its own. People also talk about two-speed development, although that version tends to be read as two separate teams, which is exactly what does not work. The name matters less than the operating rules.
Applied to the interface layer, the order I follow is tokens first, with the inherited implementation consuming them underneath, so the old CSS keeps working but already reads from the new place. Primitives with no domain knowledge come next, and the coupling is resolved by composition in the layer above, never inside the component.
What makes the two tracks work, more than the replacement plan itself, is four operating rules:
The long track’s capacity is reserved, not requested. A fixed share of the sprint, between 20% and 25%, committed like any other delivery. The moment it has to be justified every two weeks, it disappears in the first quarter under pressure.
The same people work both tracks, rotating. If the long track is a separate team, it produces a technically correct library that nobody adopts, because nobody who built it has lived through the nine-day deadline.
The short track does not fix the old, but it does not extend it either. Everything it touches comes out consuming the new. This is the rule that makes the replacement advance on its own: the long track builds the piece, and the first campaign that passes through that area is the one that puts it into production.
The long track ships every iteration, never on a long-lived branch. If something takes three months to reach production, it is not a long track, it is a parallel project under another name.
The alternative I rule out is precisely that one: the new library built clean in parallel while the product carries on with the old one. I rule it out for a reason that repeats in every case I know of: the product does not stop. While the clean library is being built, campaigns keep shipping on the old one, and eighteen months later there are two live systems instead of one old one. The migration that was going to be the last step ends up being a project the size of the original, and it arrives when nobody remembers why it was approved.
The price of two speeds has to be stated in the meeting where it gets approved, not afterwards. The short track delivers between 20% and 25% less than it would if it had the whole team, and that shows from the first month, while the return takes three or four quarters to become visible. During that time two ways of writing interface coexist and the team has to know which one it is writing in each time, which is exactly the kind of ambiguity that produces decay. That is offset with executable rules rather than training: if a layer is frozen, the build has to fail when somebody writes in it.
The limit of validity is the continuity of judgement. This holds up if somebody with authority over the architecture stays for the whole run and can turn down the shortcut when the December campaign arrives, which it will. If that figure dissolves halfway through, what is left is a fifth layer on top of the previous four, and it would have been cheaper not to start. On a product that will not outlive three years, or on a team small enough to fit in one conversation, it does not pay off either: there the consistency is held by people’s memory, and banning hex values inside components is enough.
The question I use to tell one cost from the other is one I always ask in the first session: about this thing you call debt, can anyone tell me when it was taken on and what was agreed to pay it back? If there is an answer, there is a repayment plan and the conversation is about scheduling. If nobody can answer, there is no debt to repay: there is an architecture that was never decided, and that is not paid off in one iteration, it is replaced across several.