ES
Article19 Oct 202621 min readopinion

Why Nobody Wants to Maintain Your System

Code smells, god classes, config sprawl: the design smells that make everyone afraid to touch your system, how they happen, and what's behind them.

Contents

There’s a very specific moment in the life of a project when you realize something’s wrong. Not because it crashes, and not because it’s throwing errors left and right. No. You realize it because you say out loud, “we need to touch this module,” and suddenly everybody’s looking the other way. Nobody makes eye contact. Someone gets up “to grab a coffee.” Someone else remembers a doctor’s appointment. And the junior dev mysteriously vanishes from Slack.

Your system works… but nobody wants to lay a finger on it.

That, my friend, is the clearest sign that your design stinks.

Today we’re going to talk about why that happens. About the maintainability “smells” that make any developer with a survival instinct run for the hills. What those systems look like on the inside, the stories behind them, and how you ended up there without noticing. And yes, there will be jokes, because otherwise you’d end up crying.


That funny smell: what a “code smell” really is

Let’s start by naming the monster, without getting stuffy about it.

A “code smell” is nothing more than that “something stinks here” feeling you get when you look at a piece of code or an architecture. It doesn’t necessarily mean there’s a bug today. It means that, the way things are, maintaining it is going to be painful, and that there’s probably a questionable design decision lurking behind it.

Kent Beck coined the term in the late nineties, and Martin Fowler made it famous in 1999 with a book called Refactoring. In case the names don’t ring a bell: Beck is the father of Extreme Programming and of test-driven development as we know it; Fowler is probably the most widely read software architecture writer in the world. Both signed the Agile Manifesto. In other words, these aren’t two random guys sounding off on a forum.

That book has a chapter the two of them wrote together that’s basically a catalog of bad smells: each one with its name, how to spot it, and which refactoring gets rid of it. Half the industry learned to talk about badly designed code using that vocabulary. And the core idea is this: code smells are symptoms, not final diagnoses. It’s like walking into a room and noticing it feels damp. You can’t see the leak yet, but you know something’s not right with the ceiling.

Every review of code smells lists the usual suspects: giant methods that do everything, god classes that know about everything, tangled dependencies that turn the code into spaghetti, logic more interested in another class’s data than its own, configuration scattered across ten different places… and, in general, anything that turns every change into an occupational hazard.

So let’s get our hands dirty with examples every developer has seen, suffered through, and quite possibly written. If you’ve been in this line of work for a while, you know exactly what I’m talking about.


The Frankenstein method: when one function does everything

I bet you’ve had to face “the method” at some point. That one. The one that opens the file, runs the validations, calculates taxes, sends emails, updates three tables, writes to the log and, if you’re not careful, turns off the office lights on its way out.

The infamous “long method” is one of the oldest and most studied smells. A 2018 study of thirty open source projects found that long and complex code smells are among the most widespread, and that the classes that have them change more often and pile up more defects than clean ones.

The explanation is simple: if a method hundreds of lines long does everything, nobody understands the whole thing. It’s harder to see which part affects what. Every time you touch something, the blast radius, as some people call it, goes through the roof: you change one condition and break a case you didn’t even know existed.

Picture an order controller in an online store. In theory, it should orchestrate four things: validate, calculate, save, notify. In practice, you find a 400-line processOrder() method. Inside there’s discount logic, loyalty logic, a direct database query because “it was faster to do it right there,” and a couple of if statements with comments like // TODO fix this properly.

The result? Every time marketing decides to change a discount rule, nobody wants to touch processOrder(). If the method fails, it takes down 80% of the use cases. If you add tests, covering everything is a nightmare. If you want to extract something, you don’t know where to start.

Every article and talk about “long methods” says the same thing: they’re hard to understand, hard to test, and hard to reuse; and they tend to mix several levels of abstraction, blending low-level details with high-level decisions. In other words, they’re perfect machines for wearing out whoever has to touch them.

The reason they end up like this is very human: deadlines. You start with a small method, add two special cases, then another one, then a quick if “just for now,” and before you know it you’ve got a novel inside a function. And nobody dares to be the first one to tear it apart.


The God Class: when your design builds its own Vatican

Let’s go up a level. If the Frankenstein method is a classic, the “God Class” is a full-blown academic meme.

A God Class is that class that seems to have an opinion about everything happening in your application: it knows every model, every state, it has a thousand attributes and a thousand methods, half the codebase depends on it, and if you touch it, you can hear a scream from production somewhere in the distance.

The concept isn’t new: Arthur Riel was already talking about “the God Class problem” in Object-Oriented Design Heuristics, a book from 1996. And since then, plenty of studies on code smells have ranked it among the worst: it centralizes too much control and state, it becomes a bottleneck for the system’s evolution (every change, wherever it comes from, ends up going through it), and it tends to concentrate so much change that it ends up riddled with bugs. There are even gists and guides with advice along the lines of: “if your class has several thousand lines, that’s a huge smell; start extracting dependencies into new classes and take the code with them.”

Think of the typical do-it-all ApplicationService or Manager: the UserManager that creates users, deletes users, sends emails, manages permissions, resets passwords, handles social login, manages tokens, writes logs, and makes breakfast. Or the legendary OrderService, which takes care of everything related to orders, payments, returns, inventory, logistics, and the weather on Mars.

Who wants to maintain that? Nobody in their right mind.

Technically, what a God Class does is blow right past the single responsibility principle. That’s the S in SOLID, the acronym popularized by Robert C. Martin, the famous “Uncle Bob,” which comes up in every technical interview. The principle is very simple: a class should have only one reason to change.

Think about the UserManager from before. It changes if marketing wants a different welcome email. It changes if security tightens the password policy. It changes if the login provider tweaks its API. It changes if someone wants a different log format. Four teams, four reasons, one class. And each team’s change can break what the other three rely on. That’s why the recipe is always the same: split it into smaller classes, each with a single reason to change. It’s not about aesthetics: it’s about no longer having four teams stepping on each other’s toes in the same file.

These classes usually show up because of poor planning and weak architecture, and it’s very common for a system to have “two gods”: the user class and the class at the center of the business.

The most common cause is that obsession with “centralizing” because “that way everything’s in one place.” You start moving logic from other layers into a shared class “to avoid duplication.” Little by little, everything you don’t know where to put ends up there. And in the end, you’ve got a capricious deity that rules over everything and breaks everything.

And of course, nobody wants to be the heretic who touches the god class. Too much risk, too little glory.


Tangled dependencies: the spaghetti of microservices and libraries

Let’s move on to the architectural level. There’s a dead giveaway that your design stinks: when your dependency diagram looks like a web spun by a drunk spider.

In monolithic code, this shows up as circular dependencies between modules or libraries: module A depends on B, B depends on C, and C depends on A. Or even crazier things, like libraries that import each other. A very simple example that pops up in articles about code smells: library X depends on library Y, and Y depends on X. That makes building, deploying, and testing harder, and it makes reusing anything on its own practically impossible.

In the world of microservices, the spaghetti gets even more fun. On paper, each service is going to be independent, small, with clear boundaries. In practice, without decent domain design, you end up with services calling each other in loops, business flows that cross ten services to do something a monolith used to do with three database calls, and latencies that are frankly laughable.

And there’s a variant of this smell that doesn’t come from your code, but from other people’s. Folks in microservices security talk about “tangled dependency trees”: hundreds of third-party libraries mixed in with your own services, where a vulnerability in a single dependency can affect the entire system. The textbook example is Log4Shell. In December 2021, a vulnerability showed up in Log4j, a Java logging library used by half the planet: all it took was for your application to log a string crafted by the attacker (a username, for example) for them to run code on your server.

But the interesting part for us wasn’t the patch. It was what happened before the patch: a lot of companies took days, or weeks, to figure out where they were even using it. They hadn’t pulled it in themselves; it was buried in a dependency of a dependency. It’s the same problem as your microservices, at a different scale: you don’t know what you depend on until it breaks.

Others, from the architecture side, tell stories about systems where microservices had been built with no discipline at all. The result: a complex web of calls between services, with undocumented dependencies, where every new service added one more edge to the graph until nobody could draw the whole thing without breaking down in tears. The fix they usually propose is the standard one: map the entire architecture with distributed tracing tools, group services by domain, and cut unnecessary dependencies.

Behind all of these cases is the same thing: no clear boundaries. Poorly defined domains, short-term convenience, “I’ll just make a quick call to this service, what’s the harm.” And in the end, a system where any small change can have unexpected consequences in places you didn’t even know were connected.

The practical result? Nothing can be changed without fear. And so, nobody wants to maintain it.


Config everywhere: sprawl fest

Another subtle smell, the kind you don’t notice until it blows up in your face, is scattered configuration.

It starts innocently enough. You have a config.yml. Fine.

Then you add a “quick” environment variable so you don’t have to touch the file. Then you add a configuration table in the database. Then another team adds flags in an internal admin panel. And before you know it, you change a value in one place and the system keeps behaving exactly the same, because that value gets overridden from somewhere else nobody remembers.

In the infrastructure and cloud world, this is flat-out called “configuration sprawl”: configuration spread across a thousand places, changed by different teams, with no central view, until you get security holes, outages, or audit scares. The term comes from urban planning: “urban sprawl” is when a city grows outward like crazy, one development after another, with no plan and no center.

And this isn’t just inconvenient; it’s dangerous. When configuration lives in a thousand places, nobody reviews all of it, and that’s exactly where values that shouldn’t be there slip through. Several recent analyses of cloud misconfigurations say they’re among the most common causes of data breaches and outages: exposed storage, overly generous permissions, insecure defaults, and nobody noticing because “everything seemed to work.” Back in 2019, Gartner went as far as predicting that through 2025, 99% of cloud security failures would be the customer’s fault, not the provider’s. And almost always because of configuration.

The same thing happens with application configuration. If your timeouts live in four different places, if your endpoints depend on three layers of values overriding each other, if you have flags that turn on through weird combinations of values in the database, nobody knows which configuration is actually live in production. Every change becomes a leap of faith.

Another modern variant is “secret sprawl”: secrets (tokens, keys, passwords) scattered across countless repos, services, and pipelines, until it’s impossible to track them all down. Some experts are already warning that when secrets spread like this, it’s no longer just a security problem; it’s a business continuity problem. If you don’t know where your own secrets are, you also don’t know what breaks when they expire, get revoked, or when you switch providers.

And what does this have to do with maintainability? Everything.

If nobody understands where the values that govern your system come from, changing its behavior is a nightmare. Troubleshooting incidents turns into a scavenger hunt. And the classic “it worked in staging but not in production” stops being a joke and becomes your daily life.

Config sprawl is the organizational equivalent of that junk drawer full of cables at home: you know there’s an HDMI cable in there somewhere, but you’d rather go buy a new one than stick your hand in. Same thing with your system: nobody wants to be the one who touches the global configuration.


Too many concepts: when your domain model is a museum

Less talked about, but just as painful: the “too many concepts” smell.

You run into it when you open up a codebase and discover that, to represent something simple, there are five different types. Take something as innocent as “customer”: you’ve got Customer, Client, User, Account, Profile… all with similar fields, similar relationships, similar uses, but not quite the same. Some are used in billing, others in marketing, others in authentication. And none of them fully matches the others.

In academic reviews of code smells, this is called “redundant models”: several classes that represent the same thing and make the domain harder to understand. And it usually comes with a little cousin, “data clumps”: groups of data that always travel together but that nobody has put into a class. Street, number, city, and zip code passed around as four loose parameters through twenty methods, instead of living in an Address class. When you’ve got five customers, you can bet good money the address is scattered like that in three of them.

But beyond the theory, the effect is very human: any new developer gets lost. They don’t know which class to use. They don’t know where to add a field. They don’t know whether changing an attribute in one place is going to leave some other object out of sync somewhere else.

Again, the reason is pretty simple: a lack of conceptual discipline. Every team, every developer, every sprint adds its own concepts without cleaning up the old ones. Nobody stops to say, “we already have this concept, let’s use the same one.” Everyone improvises. And in the end, the domain turns into a museum of half-duplicated sculptures.

Careful, though, because there’s a catch here. The fix isn’t to merge all five into a single giant Customer with every field under the sun, because that’s just building yourself another god class. Eric Evans, in Domain-Driven Design, the 2003 book that made talking about domains fashionable, argues exactly the opposite: billing and authentication each having their own idea of a customer can be perfectly legitimate, as long as it’s clear where each one starts and ends. He calls these bounded contexts. What stinks isn’t having several models. It’s that nobody knows why they exist, or which one is in charge where.

Who wants to maintain that? Nobody. Because every decision requires studying half a dozen types before you even know where to dig in.


The system nobody understands: when even AI gives up

Let’s bring all these smells together in one scene.

A team decides to move fast and adopts AI-generated code to build a big chunk of the application. For six months, the machines crank out classes, services, helpers, and configs at a pace that makes everything look like it’s moving along nicely.

The problem shows up when someone wants to add a relatively simple feature. And they write about it: it took them three days to do something that, in theory, should have been half a day’s work. Why? Because the code in front of them was a tangle of long functions, huge classes, duplication, redundant models, and hidden coupling. There was no “big picture.” Not the person, not the tool that had generated it, nobody had a clear idea of how it all fit together.

The author describes it like this: walking into that codebase felt like getting hit by the entire catalog of code smells at once. Long methods, god classes, data clumps, feature envy, duplicated logic… the complete collection. Every change meant digging through layers of automated decisions nobody had ever reviewed.

This story is interesting because it shatters a modern illusion: no, the problem isn’t just that humans write bad code. If you don’t design boundaries, if you don’t keep your concepts and architecture under control, you can produce bad smells just as well with AI as with a keyboard and a cup of coffee. Tools amplify your discipline or your sloppiness; they don’t replace it.

And from a maintainability standpoint, the result is the same: nobody wants to go in there. Not humans, not AI agents. It’s a system that has literally become hostile to intervention.


What happens to your team when the design stinks

So far we’ve talked about code and architecture. Let’s talk about the people for a moment: they’re the ones who end up falling back on “nope, better let someone else handle that.”

When a system piles up all these smells, something very specific happens: people stop trusting that they can change it without breaking it. Every refactoring is Russian roulette. Every new feature drags on because you first have to understand half the system just to change two lines.

The maintenance literature keeps insisting that code smells are indicators of structural problems that make evolution harder: more smells, more defects, more time per change, more frustration. And when several smells coexist in the same class (say, a God Class with other smells piled on top), things get worse: in a study where fourteen developers removed sixty of these combinations, what they struggled with most wasn’t the refactoring technique, but understanding the code.

You can see it in very human things:

The senior who always signs up for new projects but dodges the old system any way they can. The junior who gets assigned to “fix a couple of things” in the god class and ends up having nightmares about it. The ops person who knows the configuration is a mess and would rather not touch anything unless something’s on fire.

A culture of technical fear takes hold: nobody makes big changes, patches pile up, important decisions get postponed because “we might break something.” And every postponement adds another layer of smell.

Suddenly, it turns out the problem isn’t that people are lazy or difficult. It’s that your system is, objectively, hostile to maintenance. And people protect themselves.


They don’t hate your system, you’re asking them to move into a house that’s falling apart

The final image I want to leave you with is a very visual one.

Your system isn’t a temple of perfect architecture that people look down on out of spite. In many cases, it’s more like an old house where, every time you lift a floor tile, out comes a wire leading who-knows-where. Some walls are propped up with wooden beams. There are rooms that have been sealed off “because nobody’s gone in there since 2012.” And every now and then you hear a noise in the pipes.

And then you’re surprised nobody wants to move in. That everybody would rather work on the new construction going up two blocks away.

The clear signs that your design stinks – giant methods, god classes, circular dependencies, scattered configuration, too many duplicated concepts – aren’t just problems “of elegance.” They’re practical reasons why people would rather not touch your system.

The line between a system with character and an unmaintainable system is pretty thin.

And yes, the process can be reversed: refactor, cut dependencies, unify models, centralize configuration in a sensible way. And you don’t have to invent anything: there are entire books on this. If you have to start with one, start where it all began: Fowler’s Refactoring. Besides putting a name to every smell, it walks you through getting rid of each one step by step, in small changes, without breaking anything along the way.

And if your falling-apart house has no tests, which is the most common situation, the book is Working Effectively with Legacy Code, by Michael Feathers. Feathers defines legacy code in a way that stings: code without tests. Doesn’t matter if you wrote it last week.

And here’s the connection with the fear we were talking about earlier. People don’t avoid touching the code out of cowardice; they avoid it because there’s no safety net. Without tests to tell you what you’ve broken, every change is a leap into the void. Feathers’ proposal is to start right there: write tests that pin down what the system does today, even if it does it wrong, and only then start breaking it apart. With a net, people jump.

But the first step is accepting the obvious: if every time you say “we need to touch this module” people disappear… maybe the problem isn’t the team’s attitude.

Maybe the problem is your design.

And the next time you’re tempted to slip “just one more if” into the Frankenstein method, or sneak “one more bit of logic” into the god class, think about your future self, staring at the code and thinking: “what kind of animal wrote this?”

And realize you’re about to answer your own question.


Quick glossary

The dictionary nobody bothered to write because everyone assumed you already knew.

  • Code smell: a sign in the code that points to a possible design problem. It’s not a bug in itself, but a symptom that something could make maintenance harder.
  • Feature envy: when a function constantly accesses another class’s data instead of its own. A sign that the logic should live in that other class.
  • Data clumps: groups of data that always show up together in different places in the code (parameters, attributes) and that could be wrapped in a single object or class.
  • Single responsibility principle: the S in SOLID. A class should have only one reason to change.
  • Bounded contexts: in Domain-Driven Design, explicit boundaries within which a model (for example, “customer”) has a specific meaning. Two contexts can have different models of the same concept without that being a smell.
  • Legacy code: according to Michael Feathers, code without tests, regardless of how old it is.
  • Blast radius: how many things break when you touch one piece. The bigger the radius, the worse the isolation in the design.

Sources and references

The smell shows up before the bug. These sources are the deodorant.

Other languages: Español