Skip to main content
Back to Blog
Engineering

The Case Against Over-Engineering (From Someone Who's Done It)

December 1, 202510 min read
ArchitectureOver-EngineeringYAGNIBest PracticesDesign

The Case Against Over-Engineering (From Someone Who's Done It)

I have a confession. In 2023, I spent three weeks building a plugin system for a test automation framework. Configurable test runners. Hot-reloadable plugins. A dependency injection container. The whole thing.

Nobody ever wrote a plugin.

The framework ran in CI with the same configuration every time. The "extensibility" I built was used by exactly zero people. I could have shipped the entire thing in 4 days without the plugin architecture.

How Over-Engineering Happens

It starts with a reasonable thought: "What if we need to extend this later?"

That thought is the trap. Because "later" rarely looks like what you imagined, and the abstractions you build for imaginary requirements usually get in the way of the real ones.

Here's the progression I've watched in myself:

  1. Build a simple function ✅
  2. Think "this should be configurable" ⚠️
  3. Add a config object
  4. Think "different environments might need different implementations" ⚠️
  5. Add an interface and factory pattern
  6. Think "we might need to swap this at runtime" 🚩
  7. Add dependency injection
  8. Realize nobody has ever needed to swap it
  9. Maintain the abstraction forever because removing it is harder than keeping it

The Three Questions

Before adding any abstraction, I now ask:

1. "Has anyone actually asked for this?"

If the answer is "no, but they might" — don't build it. YAGNI (You Aren't Gonna Need It) is the most violated principle in engineering.

2. "What's the cost of adding this later vs now?"

If I can add the abstraction in 2 hours when it's actually needed, there's no reason to build it now "just in case." The cost of premature abstraction (maintaining code nobody uses) is almost always higher than the cost of adding it later.

3. "Can I explain why this exists to someone in one sentence?"

"We use dependency injection because we need to swap the payment provider between Stripe and Braintree in different environments." That's a real reason.

"We use dependency injection because it's best practice." That's not a reason. That's cargo culting.

What Simple Code Looks Like

```python

Over-engineered

class NotificationService: def init(self, provider: NotificationProvider): self.provider = provider

def send(self, notification: Notification):
    self.provider.send(notification)

class EmailProvider(NotificationProvider): def send(self, notification): # 200 lines of email logic

class SMSProvider(NotificationProvider): def send(self, notification): # never implemented, never will be

Simple

def send_email(to: str, subject: str, body: str): # 30 lines that actually send email ... ```

The simple version is readable, testable, and does what it says. If you ever need SMS, add a `send_sms` function. Don't build the architecture until you need the architecture.

When Abstraction IS Worth It

I'm not saying never abstract. Abstraction is valuable when:

  • You have 3+ concrete implementations. Not 1 with an interface. Not 2. Three. That's when patterns emerge naturally.
  • The abstraction removes duplication. If 5 test files copy the same setup code, a fixture is justified.
  • The abstraction is well-understood. Page Object Model for Selenium? Yes. Custom reactive framework? No.

The Nexural Lesson

The Nexural platform has 185 tables and 69 API endpoints. You'd think it's heavily abstracted. It's not.

Most API routes follow the same 5-line pattern: validate input, query database, format response, handle error, return. There's no "BaseController" or "ServiceLayer" pattern. Each route is a standalone function.

This means I can read any route and understand it without tracing through 4 layers of abstraction. When a route needs special behavior, it has special behavior — right there in the file, not hidden behind an interface.

185 tables. Zero abstract base classes. And it works just fine.

The Rule I Follow Now

Don't design for the future. Design for clarity.

Clear code can be refactored into any pattern when the need arises. Abstract code can only be understood by the person who wrote it — and even they forget why after 3 months.

Want to see this in action?

Check out the projects and case studies behind these articles.