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:
- Build a simple function ✅
- Think "this should be configurable" ⚠️
- Add a config object
- Think "different environments might need different implementations" ⚠️
- Add an interface and factory pattern
- Think "we might need to swap this at runtime" 🚩
- Add dependency injection
- Realize nobody has ever needed to swap it
- 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.