Keep It CLEAR: A Pragmatic Guide to Clean, Testable Code

Julio Falbo11 min read
Listen to it, read by Julio Falbo (14 min)
14:00

Hello people!

Lately I’ve been seeing a lot of conflicting advice about writing clean code. On one hand, people say “Don’t over-engineer, keep it simple!” and on the other hand, “Follow all the SOLID principles and design patterns!”.

It can be confusing to figure out the right approach. In this article, I want to clear up these contradictions and share a balanced strategy I use called CLEAR. It’s a set of principles that helps me write code that is easy to maintain and easy to test, without unnecessary complexity.

Let’s dive in!

Avoid Over-Engineering (KISS & YAGNI)

First, let’s address the urge to over-engineer. You might have heard of KISS (Keep It Simple, Stupid), which basically means don’t add complexity that isn’t needed. This goes hand-in-hand with YAGNI (You Aren’t Gonna Need It), a reminder to only build what you need right now.

In short: Don’t write code for hypothetical future requirements. Over-engineering violates these principles by introducing layers, abstractions, or “clever” solutions with no clear benefit. The result? Code that’s harder to understand and maintain, with no real payoff.

Resist the temptation to add every pattern or abstraction by default. For example, some developers create an interface for every class “just in case” they might need it in the future. In reality, doing that everywhere is overkill.
By adding unnecessary indirections and layers, you make the codebase more verbose and complex without any actual gain.

Instead, keep things simple.

Apply patterns judiciously , only where they add concrete value. If a class is unlikely to ever have multiple implementations, or if it’s just a plain data holder, creating an interface for it provides no advantage. It’s like using a bazooka to kill a fly, overkill!

You can always introduce an abstraction later when a real need arises (that’s the essence of YAGNI). The goal is to fulfill today’s requirements with lean, straightforward code and no extra baggage.

Remember: do the simplest thing that works, and only abstract or extend when you truly need to.

Use Interfaces (and Abstraction) Judiciously

So, how do we reconcile the “keep it simple” advice with the idea that we should “code to interfaces” or use abstraction?

The key is context.

Not every part of your system needs an abstraction, but some parts definitely do benefit from it. Interfaces and abstract classes are great tools for decoupling modules and providing flexibility, but you should apply them in the right places.

But Julio, where interfaces shine?

The answer is: In application layers like services or repositories, using an interface to define the contract can be very useful. These are parts of the code that may have different implementations (e.g. swapping databases or using a mock for testing) or that you might want to decorate (for example, adding caching or logging around a service).
In such cases, coding to an interface decouples what is done from how it’s done, making future changes easier.

In fact, core layers like complex services and persistence classes should expose an interface for exactly this reason. This lets you introduce alternative implementations (like a fake repository for tests, or a new data source) without altering the code that uses them.

It also enables advanced patterns like Aspect-Oriented Programming (AOP) proxies or decorators (heart of Spring Boot), if you want to wrap a service with logging, having it implement an interface makes that plug-and-play.

Ok, I got, so tell me now where interfaces hurt more than help.

Of course, so, there we go!

For simple data-carrying objects or value objects, an interface is usually pointless. A data entity like User or a value object like Coordinates typically has no behaviour that varies; it’s just data (maybe with basic validation).
Creating IUser or ICoordinates adds indirection with no gain, since you will never have multiple “implementations” of what is essentially a data structure.
In these cases, an interface doesn’t make the code more flexible or testable, it only adds noise and extra files to manage.
You can just instantiate and use the object directly in both production and tests.
So in other words, model/value objects (like Person or Address classes) should not have interfaces. It simply doesn’t buy you anything.

So, you must find the right balance, use interfaces (or abstract classes) for the parts of your system that are likely to change or benefit from multiple implementations, and keep the rest of the code simple and concrete.

There is no one-size-fits-all rule, the gray areas require good judgement. We shouldn’t fall into dogma like “never use interfaces” or “always use interfaces everywhere.” Instead, apply abstraction where it genuinely makes your design cleaner or more adaptable, and avoid it where it only adds clutter.
This balanced approach eliminates the apparent contradiction: we’re not advocating two opposite extremes, but rather a cohesive guideline.

In summary: don’t over-engineer with needless abstractions, but do introduce interfaces when they truly add value.

The CLEAR Approach to Clean Code

To help keep my code maintainable and coherent, I follow a set of design guidelines summed up by the acronym CLEAR. This stands for:

These CLEAR principles complement each other. If each class has clear responsibilities and you keep coupling low, it naturally sets you up to achieve more reuse (because modules are independent and focused, so you can mix-and-match or share them easily). Encapsulating changes and abstracting dependencies go hand-in-hand to create a codebase that’s flexible but not fragile. The overarching idea is consistency: you get a unified, intentional approach to design rather than a bunch of ad-hoc decisions. By keeping your code CLEAR, you prevent the scenario where one part of the code says “do X” and another part contradicts it by doing “Y” differently. Instead, everything follows the same guiding principles, making the whole system easier to understand and evolve.

How CLEAR Leads to Easier Unit Testing

One big bonus of writing code in a CLEAR way is that it becomes much easier to unit test. Clean and decoupled design means you can test components in isolation, without a lot of setup or special tweaking. Here are a few ways the CLEAR principles make your code more test-friendly:

Overall, designing with CLEAR principles leads to a codebase where each piece is independent and predictable, which is exactly what you want for unit testing. You can instantiate a class, inject a couple of fake dependencies, call a method, and assert the result, without needing a whole application context. The tests stay fast, focused, and easy to debug when something fails. When writing tests is straightforward, you’re more likely to write them for all your code, meaning you catch bugs early and can refactor with confidence. In short, clean design == easier testing, and easier testing means better software quality.

Conclusion

Great software design is a balancing act. We want our code to be simple and clear, but also flexible and robust to future changes. The apparent contradiction between “don’t use interfaces everywhere” and “you should code to interfaces” disappears when you apply contextual thinking. Use the right tools in the right situations. The CLEAR principles help navigate this balance by emphasizing clarity, appropriate abstraction, and modularity without veering into over-engineering. By keeping your code lean, focused, and well-structured, you make it easier for yourself (and others) to understand and modify it down the road. And by selectively applying interfaces and other abstractions where they add value (like allowing multiple implementations), you ensure your code is prepared for growth and change when it comes.

At the end of the day, writing clean, testable code comes down to being deliberate.

Don’t follow every rule or pattern blindly, understand the why behind a principle and use it when it genuinely improves your software.

Keep your codebase CLEAR, and you’ll find it stays coherent, easy to maintain, and ready for whatever the future throws at it.

Happy coding!

References

  1. “You aren’t gonna need it” (YAGNI) — Extreme Programming principle. Wikipedia.
  2. Jason Gilmore (Aug 22, 2023). What is Loose Coupling in REST APIs? — DreamFactory Blog.
Share on LinkedIn

Originally published on Medium on Oct 15, 2025. Claps and comments live there.

Comments

No comments yet. Be the first.

We email you a 6-digit code to publish it. Your email is never shown.

← More articles · follow by RSS