varakh.de

If it's too complex, kill it

When coding - whether professionally or as a hobby - you’ve likely found yourself staring at your work thinking:

“What the hell did I write there?”

Or worse: the code was written by someone else. You’re not alone. This frustration stems from how codebases evolve. Features get added quickly, hotfixes are rushed to production, and contributions accumulate. Gradually, your once-readable code becomes a tangled mess.

You might think: “just add comments”. Don’t. Heavy reliance on inline comments to explain logic often signals code that needs refactoring. Exceptions exist - method documentation and complex algorithm explanations are valid, but verbose step-by-step comments usually mask unclear code.

Programming languages and frameworks are precise by design. There’s no room for interpretation. We’re engineers, we’ll understand it. While documentation, onboarding, and experience help us understand code, complexity remains the ultimate barrier though. Simple code is inherently easier to understand, extend, and maintain. The less complex your code is, the better you and others will understand (and extend and maintain) it.

I’ll try to share my thoughts on complexity:

Rather prioritize clarity over cleverness, and you’ll build systems that stand the test of time.

Abstraction layers

Does your code need to be that complex?

I’ve seen this pattern repeatedly and still fall into the trap myself. We over-engineer solutions because they’re “clever” or “future-proof,” adding layers like “X might need this later.” But ask yourself:

If your answer is “Yes, I can live without it now,” delete that complexity immediately. Future-proofing often becomes future-burdening.

Example?

1public interface MyEndpoint {
2    PostResponse createPost(CreatePostRequest post);
3}
4
5public class MyRestEndpoint implements MyEndpoint {
6    public PostResponse createPost(@Valid @RequestBody CreatePostRequest post) {
7        // some logic to create the post in a service
8    }
9}

This Java example looks pretty straight forward, right? Well, it’s clean, no doubt. But why do we need an interface here? Do you really anticipate to add more implementations to create a post in your small back-end in addition to the RESTful way? No? Kill it.

Don’t over-do interfaces or abstractions. Introduce them when necessary.

That was quite an easy example. It gets worse when you see entire applications being introduced just for the sake of abstraction. Step back, think about it twice if it’s really serving any benefit despite fulfilling your engineer’s dream.

Dependencies

We all love and hate them. We love them because they eliminate the need to write code yourself. Just plug in that tool, and it handles everything without requiring a single line of custom code. We hate them when they break or demand maintenance.

Don’t reinvent the wheel, but for every dependency you add, ask:

If the answer is no to any of these: Kill it.

Derived from this, it should be common sense, but here’s why you should never add unused dependencies. Adding unused dependencies to your project is a major no-go. They introduce unnecessary security risks, increase code bloat, slow down builds, and create extra maintenance overhead. Even if you’re “preparing for something”, unused dependencies expand your attack surface and complicate your codebase without delivering any real benefit.

To keep your software lean, secure, and efficient, always remove dependencies you don’t actively use. Regular cleanup helps prevent vulnerabilities, reduces build times, and makes your project easier to maintain. Remember: less is more when it comes to dependencies! It’s about reducing, not adding complexity.

Shared code

Code duplication remains a hotly debated topic in software development, balancing the need for flexibility against long-term maintainability.

While the “Don’t Repeat Yourself” (DRY) rule is foundational, blind adherence can lead to over-engineering. Short, stable code snippets or temporary prototypes might justify duplication, but uncontrolled copying creates technical debt through inconsistent logic and hidden bugs.

Adhering to the following might help balance it for you:

  1. Libraries:
    • Best for: Stable, widely-used utilities.
    • Pros: Single source of truth for shared code.
    • Cons: High maintenance overhead, poor fit for volatile code.
  2. Git submodules:
    • Best for: Large, semi-stable dependencies (e.g., shared API clients).
    • Pros: Atomic updates, separation of concerns.
    • Cons: IDE tooling challenges, might feel “alien” compared to a library.
  3. Plain copy:
    • Best for: Experimental features or highly contextual logic.
    • Pros: Zero abstraction cost, isolated changes.
    • Cons: Manual synchronization, version drift risks, actual duplication.

When choosing an approach, inspect expected change frequency (libraries for low-churn code, copies for high-churn), cross-project use (libraries/submodules for shared code, copies for project-specific needs), and team expertise (submodules require more Git mastery while libraries demand more packaging skills).

If you find yourself duplicating code, ensure to track origins with comments, as description of a git message or document it externally with proper reasoning. As a rule of thumb, abstracting at the third duplication might serve useful to avoid complexity. In my opinion, the real danger lies not in duplication itself, but in unmanaged duplication. If you (and everyone who needs to) know where code requires synchronization, is it an issue by itself?

Tooling

Modern development offers endless tools to “simplify” configuration management, deployments, and workflows, but each new addition introduces complexity. Are you using tools to solve problems, or just creating new ones?

Ask yourself:

Audit your toolchain. Remove underused tools, consolidate overlapping ones, and favor transparency over “black-box” solutions. Hot take, but sometimes, a well-documented shell script beats a bloated framework.

CI/CD complexity

When building modern applications, you’ll typically rely on a pipeline to handle building, testing, and shipping your code. Whether you use GitHub Actions, Jenkins, or other tools, be mindful of the complexity your pipeline introduces. If maintaining it becomes a burden, it’s likely too complex!

Avoid letting your DevOps team dictate which tools you must use. Don’t work around artificial limitations - implement CI/CD in a way that aligns with the application’s requirements and expected environment. As the domain expert, you possess the deepest understanding of your application’s requirements and operational needs - knowledge your DevOps team might lack. Collaborate rather than circumvent. You and your DevOps colleagues share the same objectives: rapid, reliable, and automated delivery. Try to keep it simple. If there’s (nearly) zero difference between developing, building locally, and shipping a production artifact, then you’re spot on!

Simplify releases

Releases shouldn’t require code changes! Automate version bumps and changelogs instead. Use conventional commits (feat:, fix:, chore:) to auto-generate release notes directly from your commit messages. Tools like git-cliff do the rest. This means cleaner, faster releases, fewer mistakes, and everything’s auditable. Stop manually updating versions and doing all the merge backs – automate it!

Writing less complex applications or why 12 Factor Apps matter

If you’re like me and have wrestled with messy deployments or “works on my machine” bugs, the 12 Factor App methodology is a game changer. It lays out simple, practical best practices for building cloud-native apps that are scalable, maintainable, and resilient.

By standardizing how you handle code, dependencies, configs, and processes, it cuts down on complexity and those frustrating environment-specific issues. Plus, it encourages building stateless, easily deployable services that scale smoothly and bounce back quickly from failures.

In my experience, embracing these principles not only makes your life easier but also helps your team deliver reliable software faster. It’s all about working smarter, not harder, and keeping things simple where it counts.

Conclusion: Coding with clarity

This article emphasizes applying practical judgment to reduce unnecessary complexity in your codebase. Being practical and having a feeling for it often comes with experience.

By prioritizing simplicity, you’ll ship faster, improve maintainability, and create code that’s easier to onboard others to. Over time, refactoring replaces accidental complexity with robust, resilient systems.

The best code solves problems, not just theoretical puzzles.

#software #coding #development #engineering #complexity