6 Traps of software development
There are countless practices in software development, and one thing I've learned is that very few are universally "best practices." Taken too far or applied in the wrong context, they can cause more harm than good. This is true beyond software development. Medicine heals, but an overdose can be lethal. The same logic applies to development practices: what seems the promised land can become dangerous when misused or over-applied.
That’s what I call traps. They stem from those good practices and good intentions that are valid and sound at first sight. They do offer real benefits at first, which is why they become popular. However, their pitfalls reveal themselves when they’re followed too rigidly or applied without understanding.
Let me list 6 of the most common traps I often see. This is not a comprehensive list but being aware of those ones will help you in 80% of the cases that can lead you to the land of no return. Recognizing when a practice is helping versus when it’s becoming a liability is winning half the battle.
Trap 1: If something works do not touch it
Life is a constant fight. “Work hard” they say. Then they added, “Work smarter”. I think you need both. No pain, no gain. Those make for catchy Instagram actions but we all know -despite whether the instagramer follows through those principles, or your favorite blogger- that they are true. To achieve anything, effort is essential.
Effort is difficult. And I am quite thankful for that, it’s what separates remarkable from the mediocre. If it wasn’t like this, we’d all excel at everything. How unrewarding that would be. But since it’s hard, that’s why those mottoes will never expire. We need to remind ourselves constantly, to keep putting effort, to keep the effort consistent. To master that skill. to improve.
Of course, that “smart” part is crucial. Giving just the right amount of effort yields better results than pouring all your energy in recklessly. You need to manage with frustration too. Nobody can become an expert overnight. Mastery requires consistent effort, and frustration is a sign you’re pushing yourself. It’s part of the process. You’re finding the obstacle to overcome.
Not everyone needs to aim for mastery. If you’re content where you are, there’s no shame in that. But, if you work in any craft, then you can’t let your guard down, the fight is on and you need to be on edge, always lean into the effort side of things- or you’ll stay a beginner forever.
That’s why “if something works do not touch it” should never be taken seriously. To be fair, I’ve always heard it said in a joking way. But be careful because - it’s a trap. This saying wants to make you a hostage to ignorance, discouraging you from taking action or fighting back.
Don’t fall for this trap. It can be painful - it certainly will be- but the knowledge you’ll gain from “it” once its been touched, destroyed, dismantled, reassembled, tested, and explored will be invaluable. Be wary of those who take this saying seriously as they’ve stopped fighting. And be even more wary if that comes from leadership roles as it reflects an organizational culture that doesn’t value experimentation, and therefore innovation.
Don’t forget this principle from software delivery found in the book Continuous Delivery:
Principles of Software Delivery: If it hurts, do it more frequently, and bring the pain forward
So yes. No pain no gain. Software, like most things in life, benefits from of stoic like principles. The obstacle is the way.
Trap 2: Flexibility
Don’t fall for the trap of “flexibility” – it doesn’t exist in software! Don’t let yourself accept or agree that something will be “flexible”.
First off, flexibility isn’t one of those non-functional requirements that are desirable everywhere. In the book “Fundamentals of software development” the authors made an effort to list all the non-functional requirements, and surprise—flexibility isn’t even mentioned once.
It’s impossible to make a comprehensive list of those non-functional requirements as the options are not MECE (mutually exclusive, collectively en). Some example in the book are the following: “Operational”: availability, continuity, performance, scalabiility. “Structural”: configurability, extensability, maintainability, etc.
Nothing in the world is MECE unless you put artificial restrictions. For example, you can’t have a MECE list of all the fruits in the world, as stubborn nature will come up with some fruit that might be half mandarin and half orange, and even if you classify that as a new fruit, there’s will be the next range in between. It’s all ranges. The only way to achieve MECE is to add some artificial -human made- constraints: the fruit shop sells: “apples”, “oranges” and “mandarins”. That’s the MECE list of fruits in the shop.
So for the list of non functional requirements to be “finite” and MECE we need to add constraints. As the options where overlapping and covering different ranges of architecture. And it turns out that we can very much do so if we pick a specific point of view. And it turns out that you don’t need to sacrifice any non functional requirement in favor of others as the book would tell you. I explain all of this in this blog post.
So if flexibility isn’t a non-functional requirement, what is it? As I’ve said, flexibility doesn’t exist in software. Let’s use some examples. You either have a first-person shooter game or an e-shop selling books. Those are specific systems built to handle specific cases. How are these systems flexible? Maybe the first-person shooter game could have a third-person mode (is that a thing?), or maybe the shop could sell more products a part of books. But would that make them flexible?
Maybe that’s what is understood by flexibility, this loosening of constraints. The game has two modes now, but it’s still a shooting game at its core. The shop might broaden its use case, and its system requirements might be a little looser, but it’s still a very specific system: a shooting game with two modes or a shop selling products.
Now, let’s loosen the constraints even more. What if the game were so flexible it could attract people who don’t even like shooting games? Or, better yet, what if it could appeal to every type of gamer in the world? And the shop that sells books—well, now it should be flexible enough to do your taxes.
So for the sake of “flexibility”, we’re going to the land of avoiding definitions and decisions. Constraints are so loose that don’t exist anymore. Therefore the resulting product is an indescribable amorphous thing lacking purpose or meaning. That supposed “flexibility” brought us to the land of bad designs. We are avoiding the commitment to any design so we are not solving any problem. We try to solve all of them and failing on all of them.
And this land is dangerous to put down into code! For example, if your shop only sold books, you’d have a “sell book” use case. That’s straightforward. But if now you can sell anything, you might try adding “sell food,” “sell books,” “sell cars,” etc., or even just “sell product” to make it easier. The former is simpler to understand but not maintainable; the latter is harder to get right, especially if you still need to account for specific scenarios like the product being a book or a car.
“Flexibility” is often just an excuse to avoid making decisions, leading you to design a system that can spiral into complexity because of a “what if.”
Even out of DDD and designing a domain, the same trap exists in plain code. Can you make a library that is so flexible that does several things? Or a helper function for that matter? It’s basically violating the “Single Responsibility Principle” by definition!
So be wary of flexibility. When you hear that word, always ask twice: what exactly do they mean by that? Because it will lead you to very dark places. The price of avoiding making a decision is very costly.
Trap 3: Don’t Repeat Yourself
You were probably expecting this one, and you might have an opinion about it already. There’s already quite a lot of literature warning you about the problems with the DRY principle. It got so much hate that even the opposite “Write everything Twice” (WET) became a thing. Many books I’ve read had a side note cautioning against the DRY trap.
After all, we’ve all seen the biggest disasters, the messy balls of mud that somehow had DRY at their core (I’ve seen that as recently as last week). No wonder there’s so much backslash against it. Following DRY is easy to mess up, and it’s easy to break another important principle in the process: KISS—Keep It Simple, Stupid.
In cases of doubt, always prioritize KISS over DRY. That’s why I generally favor repetition first.
Now, DRY isn’t bad per se. So how can we balance DRY with KISS? I think the trick lies in another principle: the Single Responsibility Principle (as suggested in the blog post I’ve linked) or separation of concerns in general.
If your code does one thing and only cares about that one thing—without being coupled to other concerns—then you can reuse it, and you don’t have to repeat yourself. You might need to make it a little more generic. That’s called “making an abstraction,” and it’s essential that you get it right. You’ll get it right if your code is decoupled, and that becomes easier if you follow other principles, like Clean Architecture, SOLID, DDD tactical patterns—the whole set that I keep promoting over and over.
For instance, imagine that you are using a framework that forces you to declare your domain objects in a certain way, because that’s how they will be persisted in the database. Now that framework is coupling your domain logic with infrastructure logic, you’re in muddy waters. If you use those objects to code more than one use case (as commonly), you have to have in your head the persistence and maybe code specific “ifs” and others. Instead, if you came already clean from infrastructure (following clean architecture rule of inward dependencies) you won’t have to worry about persistence concerns, so their reuse will be way easier.
Domain objects are a simple example of following the DRY principle after all. (Just basic OOP - or you would have a script like code for each use case with a lot of repetition). Still getting them clean of infra concerns is just half of the movie. A must actually. But the right abstraction is still important in the domain. If the domain objects are not properly modeled, there will be many edge cases to cover when reusing them - the mess is still possible-.
The best quote that sums it all app is this one from Sandi Metz (which I guess is the person that appear when Googling this name):
Duplication is far cheaper than the wrong abstraction.
Trap 4: Don’t reinvent the wheel
It’s a saying full of wisdom, right? don’t waste time reinventing what’s already been made. But I’m telling you there’s a trap lurking around. The trap isn’t just about not reinventing the wheel—it’s about assuming that “not reinventing” means you should always reuse the wheel. And that’s where things can get tricky.
I hope I will not get in trouble by using this image from CQRS By Example (but I’ll take the risk).

The “framework fanboy” phase is exactly where this trap lies. It’s that phase where the solution to every problem is simply reusing a framework. Since all the systems share the same features right? Displaying data and saving it into a database, so let’s reuse all of this code, it’s already done for us!
I’ve just spend some paragraphs above arguing against flexibility. Problems will start appearing the more concrete and defined your uses cases are, and at some point you won’t be able be based on a framework. The best solution is, actually, a system that appropriately uses a framework—not one that relies on it entirely. A framework shouldn't be at the core of your system.
Avoiding the DRY trap from above, you can reuse the code, reuse the wheel, of some specific parts of the framework that deal with one and only one thing. For example, dealing with HTTP requests, that’s a responsibility the framework will fulfill better than you do. In general, all technical concerns (http requests, messaging, dependency injection) will be handled better by a framework than yourself. After all they’re normally based on open source projects with lots of contributors. Many brains beat a single brain.
But frameworks shouldn’t never handle your business logic, this is always dependent on your domain. No framework can be flexible enough for all the cases. Anyway, if the framework authors avoided the traps above, that will a framework that can be easy to reuse.
Actually, the wheel is a great metaphor to describe my point: they come in different use and sizes. A steering wheel. The tires on your car. The small, rubber wheels on your roller blades. Tractor wheels. Imagine trying to use those tractor wheels on your roller blades (I’d like to get those roller blades!). Or the other way around, a tractor with roller blade wheels, will give you nowhere.
In the world of software, the "wheel" you’re trying to reuse might be perfect for one thing but completely wrong for another. When you examine a solution, a framework, a library, whatever it is, you get the opportunity to understand how it works, and see how it fits into your needs.
For example, your building a chat application, and you come up with the best pagination system to get the messages. You are kind of reinventing the wheel, because you could learn that other chat apps often use a system called "anchor-based pagination" which is more appropriate for chat like applications.
And with that understanding you can adapt that to your needs. You’re not reinventing the wheel, good, but also not reusing whatever thing blindly, avoiding the trap. In that case the reuse is weird, but let’s say you’re not getting the full chat application, as that chat might not have all the features and requirements that the one you need should have.
And remember the first trap! Keep always on the effort side. Analyze and learn. Maybe you can come out with another kind of wheel, and make it generic enough - with proper abstractions and perfect DRY- that others can use it.
Trap 5: Optimization
Like most of the things here, optimization isn’t bad per se. But of course, it hides a trap. Since it’s a desirable thing, it’s also a common buzzword for stakeholders to use and abuse: Our processes are optimal. Our system is optimized! We always have optimization in mind and optimization is our priority everywhere!
That’s actually not a lie when following the DDD and clean architecture practices, that whole lot of practices that I am promoting always. All the time. When following those principles, you barely need to think about optimization, since you’ve spent time modeling the domain into code, that means you’ve analyzed and you’ve found the most optimal design for whatever process.
It rarely goes beyond that. I mean, nobody will ever ask you to make things more optimal if you’ve made things right. There’s only two reasons it can go beyond that: whatever thing is not modeled well enough or because you need to make it genuinely more optimal in the technical sense of way. And here’s the trap: the reason is rarely the latter.
I’ve seen people putting way too much effort into solving things and trying to make them more optimal without realizing that the whole modeling of what they were doing could be improved significantly. Making items go twice through the process. Processes doing unnecessary things. You say it. That’s why “premature optimization” is part of the “STUPID” acronym.
You should only consider a technical effort to make things more optimal if you think you can’t make it better through designing the process. It’s hard to do since optimization sells. And you will always think that maybe using that other database, or that other messaging system will be more optimal and you will be free of your problems.
The reality is that even when you’re on this stage where you think you have to rely on some better infrastructure, there’s probably other ways of designing the process that will make it more optimal. And those kind of optimizations are commonly way more effective than any infrastructure improvement.
In my opinion, infrastructure upgrades should only be considered when the whole process is code clean, based on DDD patterns and clean architecture, and the alternative of improving through design might lead to some extravagant designs hard to understand.
So yes, be careful if you’re spending time on doing things more optimal, without having any understanding of this “thing”. You might be forced in this situation (for instance, if you’re in the operations team and it’s separated from the development team, which goes against the devops principles). Know that the first step to solve a problem is identifying it.
Trap 6: Concurrency
This is optimization sister trap, as you could define concurrency as an optimization technique. You’ll rarely need it and in generally you should avoid it like the plague.
Now, there’s a lot of kinds products out there. I am always assuming a “typical” business related distributed software product. But you might be developing a database, a web server, an operating system, anything that’s closer to the technical side than the business side.
On those kind of products indeed you might need to take a different approach - still clean architecture applies and so does modeling - but those are where optimization techniques, such as concurrency won’t be so out of the question.
A “typical” business related product, when distributed, is probably already using concurrency: are you using a web server for users to execute your use cases? There you have it. It’s quite common, it’s just coded in the infrastructure layer, when you use a web server, or when you connect to a database.
That’s why the design of proper aggregate roots is so important. Aggregate roots define the transaction boundary, and proper design will make for several concurrent transactions affecting different aggregate roots to be non-problematic. Only the ones affecting the same aggregate root will need to be taken into consideration.
But still, we aren’t contemplating concurrency to solve or optimize and use case of ours. You’ll probably never need it. Only on those very specific domains I’ve mentioned above. The trap here is thinking that you might need it when you’re not on such domains.
Concurrency is a very low level feature, and if you don’t control the hardware on which your software runs, you might not gain anything out of it, and concurrency itself has some overhead. Let me share some quotes from a couple of books about programming languages that implement concurrency and what do they have to say about it.
From Learning Go
People are attracted to concurrency because they believe concurrent programs run faster. Unfortunately, that’s not always the case. More concurrency doesn’t automatically make things faster, and it can make code harder to understand [it definitely does!].[..]. Whether or not concurrent code runs in parallel depends on the hardware and if the algorithm allows it.
And from Rust for Rustaceans:
A somewhat orthogonal aspect of concurrency that you should be mindful of is the cost of introducing concurrency in the first place. Compilers are really good at optimizing single-threaded code-they’ve been doing it for a long time, after all- and single-threaded code tends to get away with fewer expensive safeguards (like lock, channels, or atomic instructions) than concurrent code can. In aggregate, the various costs of concurrency can make a parallel program slower than its single-threaded-counterpart given any number of cores! This is why it’s important to measure both before and after you optimize and parallelize: the results may susprise you
And they link this “COST” PDF for more info.
Concurrency will definitely make your code harder to understand and the gain is just potential, not guaranteed. So make very very sure that you really need it and that you’re not wandering into a Venus flytrap.
Final words
As always, the hope is to shed light on something worth considering. Seeing these pitfalls happen in the real world is both frustrating and disheartening. Developers often fall into these traps with the best of intentions, only to realize too late of the consequences. Only a small amount of critical thinking and seeing the bigger picture is needed to decide whether a practice is truly helping or quietly causing harm. The choice is yours.



