DEV Community

Mark
Mark

Posted on

Why Over-Engineering Is a Junior Developer Habit

They don't teach the real world in bootcamps. Get 25 years of production-grade experience on my YouTube: 🛠️👉 https://www.youtube.com/@lessonsfromproduction

The most complex code I've ever deleted was written by someone who'd been coding for six months.

The second most complex? Written by someone who'd been coding for six months and had just discovered design patterns.

Over-engineering isn't a sign of intelligence. It's a sign of insecurity.


The Controversial Truth Nobody Will Tell You

Not your bootcamp. Not your CS degree. Not even your senior dev in code review.

Writing simple code is harder than writing complex code.

Every junior developer I've ever mentored — every single one — went through the same phase. They learned about microservices, event-driven architecture, abstract factory patterns, and dependency injection frameworks. And then they rebuilt a to-do app using all of them.

That's not skill. That's intellectual showing off. And the worst part? They didn't even know they were doing it.


Here's Where Most Devs Mess This Up

They confuse the map for the territory.

Design patterns, SOLID principles, clean architecture — these are tools. They solve specific problems in specific contexts. But when you're new, you don't have enough experience to recognise those contexts. So instead, you apply the tools everywhere. Because it feels professional. It feels like real engineering.

I've reviewed all three of the following in actual production codebases, written by developers who were genuinely proud of them:

  • A factory pattern to create a single type of object that never changes
  • A pub-sub event system for a UI with two components
  • A microservices architecture for an app with 200 users

The Real Cost (This Is the Part Nobody Talks About)

Let's talk about what over-engineering actually costs — because it's not just code complexity.

It's time. It's cognitive load on every developer who has to maintain that system after you. It's onboarding time. It's debugging time. It's the 2 AM incident when something breaks in a layer nobody understood existed.

I've worked on systems where adding a new field to a database form required touching eleven files across four repositories. That wasn't good architecture. That was someone's vision of what good architecture looks like, implemented without any regard for the actual problem being solved.

The code worked. The team was miserable.


And This Is Why You Stay Mid-Level

There's a psychological trap at the heart of over-engineering, and it goes like this:

Simple code feels risky. What if requirements change? What if we need to scale? What if we need to swap out the database?

So you abstract. You add interfaces. You decouple everything from everything else. And now you feel safe, because you've planned for every eventuality.

But you've actually just shifted the problem. You haven't reduced complexity. You've distributed it. And distributed complexity is harder to see — which means it's harder to fix.

The most important word in software engineering is: yet.

You don't need it yet. Build for now. Refactor when the problem actually arrives.


What Seniority Actually Looks Like

Here's where most devs mess this up — and this one took me years to understand myself.

Seniority isn't about knowing more patterns. It's about knowing which patterns not to use.

The moment I realised I'd crossed from mid-level to senior thinking was when I deleted more code than I wrote in a week — and felt genuinely proud of it.

  • Pulled a twenty-step deployment pipeline down to five steps
  • Replaced a complex event bus with a direct function call
  • Removed three abstraction layers that were solving a problem we no longer had

The system got faster. The bugs got fewer. The team got happier.

Less code is almost always better code.


YAGNI: The Principle We Keep Ignoring

There's a principle in software engineering called YAGNI — You Aren't Gonna Need It.

It was coined at a time when developers were already building for imaginary future requirements. Decades later, we're still doing it.

Every time you write an abstraction for a use case that doesn't exist yet, you're making a bet. You're betting that:

  1. The future requirement will actually arrive
  2. It will arrive in the form you predicted
  3. The cost of the abstraction now is less than the cost of adding it later

In my experience, that bet loses more often than it wins.

Abstractions you add when you need them fit the problem perfectly, because you can see the problem clearly. Abstractions you add speculatively are almost always slightly wrong. And slightly wrong abstractions are the worst kind — they sort of work, so you keep building on them, and the wrongness compounds.


What "Simple" Actually Means in Practice

"Keep it simple" is advice that sounds easy and is genuinely hard to execute. So let me be specific.

Simple means: a new developer can read this code and understand what it does in under five minutes.

Simple means: there is one obvious path through the logic, not seventeen depending on which abstract interface gets injected at runtime.

Simple means: if the requirements change, you can change this code without being afraid you'll break something you didn't know was depending on it.

Simple is not lazy. Simple is disciplined. It takes more thought to write a clear, direct function than to wrap it in three layers of abstraction.

The best code I've ever read read like a very good explanation. No surprises. No cleverness. Just clarity.

// Simple
function getUser(id) {
  return db.users.findById(id);
}
Enter fullscreen mode Exit fullscreen mode

That's it. Readable in ten seconds. Done.


The Three Questions to Ask Before Adding Any Abstraction

This is the rule I give every junior developer I mentor: solve the problem in front of you — not the problem you imagine you might have in three months.

Before you add an abstraction, ask:

What is the specific duplication or coupling this solves **right now? If you can't answer concretely — don't add it.

Before you reach for a design pattern, ask:

What problem is this pattern designed to solve? Do I actually have that problem?

Before you split a service or add a layer, ask:

Who does this make life easier for — me, right now, or some imaginary future developer working with requirements that don't exist yet?


The Flip Side: Under-Engineering Is Also a Problem

To be fair — and this matters — under-engineering is also a real problem.

Copy-pasted logic spread across fifty files. No error handling. No separation of concerns. Business logic embedded in UI components. No tests because "we'll add them later."

That's not simplicity. That's chaos with good intentions.

The goal is appropriate engineering. Not more. Not less. The right amount of structure for the problem you're actually solving. And developing the judgement to know where that line is — that's what years of experience builds.


The Code Review Tell

Here's what I watch for when reviewing code.

When a developer explains their system and the explanation is simple but the code is complex — that's a problem. Good code and a clear explanation of it should look roughly the same.

If you need five minutes to explain why your architecture works the way it does, it's probably too complex. If I have to understand three different abstraction layers before I can read a database query, something's wrong.

The best developers I've worked with write code and then, when asked why they did it that way, they shrug.

"It was the straightforward thing to do."

That shrug is earned. It takes years.


The Question That Changes How You Write Code

The next time you're about to add a layer of abstraction, an interface, an event bus, or a new service — stop.

Ask yourself honestly:

Am I solving a real problem, or am I performing competence?

The developers who build the systems that last aren't the ones who knew the most patterns.

They're the ones who knew when not to use them.


25 years in. Still deleting more than I add. That's the job.

Note: This article was written with AI assistance to help structure and articulate 25+ years of debugging experience. All examples, insights, and methodologies are from real-world experience.

Top comments (0)