Chad Moran

Complexity is a Bridge

Last week I watched a team spend three days implementing a "flexible plugin architecture" for a feature that ended up being two if-statements. This is the story of every system I've ever inherited. The urge to build complexity is a bridge, not the destination. Don't build for the moon when you just need to cross the street.

Chad Moran
Chad Moran
7 MIN READ ENGINEERING / CAREER

Three Days for Two If-Statements

Last week I watched a team spend three days implementing a “flexible plugin architecture” for a feature that ended up being two if-statements.

Three. Days.

The requirements were simple: “If the user is premium, show the advanced features. Otherwise, show the upgrade button.”

By day three, they had:

  • An abstract FeatureProvider interface
  • A PluginRegistry with dependency injection
  • A FeatureVisibilityStrategy factory
  • Configuration files for “flexibility”
  • 47 unit tests
  • 800 lines of abstraction code
  • 2 if-statements doing the actual work

The ratio of functional to non-functional code? 2 lines to 800. That’s a 400:1 complexity tax.

This is the story of every system I’ve ever inherited. And here’s the thing: we all know better. We’ve all rolled our eyes at over-engineered code. Yet we keep building it.

Why? Because when we first start as engineers, we get hooked on the wrong drug. We get hooked on complexity. We learn design patterns and immediately want to use them. We read about microservices and suddenly our todo app needs twelve services. We discover dependency injection and now everything needs to be injected.

But here’s what they don’t tell you in coding bootcamp: every line of code you write is a liability. Every clever abstraction, every “future-proof” interface, every line is something that can break, something that needs testing, something that will haunt the next developer (who might be you in six months).

The decision to add complexity is a tradeoff, and it should be made intentionally. Most of the time, we’re doing it on autopilot.

The Bell Curve of Competence

Here’s something I’ve noticed after years of watching engineers grow: the complexity of systems we build follows a predictable bell curve.

Junior engineers: Simple but brittle solutions. They don’t know enough to build complex systems, so they build simple ones that break in unexpected ways.

Mid-level engineers: Robust but complex solutions. They’ve been burned by brittle code, so they wrap everything in abstractions. Interfaces for everything. Dependency injection. Factory patterns. Just in case.

Senior engineers: Robust and simple solutions. They’ve been burned by both brittle AND complex code. They know the secret: you can build robust systems without drowning in abstractions.

The journey from junior to senior isn’t about learning to build more complex systems. It’s about learning when NOT to.

The Interface to Nowhere

Let me paint you a picture. You’re building an app, and you think, “What if we need to switch databases someday?” So you build an interface. A repository pattern. Dependency injection. Mock classes for testing. An implementation class. Maybe throw in a factory for good measure.

Years pass. How many times have you actually switched databases and thought, “Thank god for that interface”?

Zero. The answer is zero.

Instead, what happens? Another developer needs to add a field. What should be a five-minute task becomes an archaeology expedition. Add it to the interface. Update the mock. Modify the implementation. Update the factory. Run the tests. Fix the tests that broke because the mock wasn’t quite right.

You didn’t just add complexity. You added friction. Every future change now has to navigate your defensive architecture. And for what? A hypothetical database migration that will never happen?

Here’s the dirty secret: when you DO need to switch databases (you won’t), the interface you built three years ago won’t match what you actually need. You’ll refactor everything anyway. All that complexity bought you nothing.

The Real Cost

Let’s put numbers on this. That database abstraction layer you built “just in case”?

  • 500 extra lines of code
  • At 15 bugs per 1,000 lines (industry average), that’s 7.5 bugs you created from nothing
  • Each bug takes 4 hours to find and fix
  • 30 hours of debugging at $150/hour = $4,500

You just spent $4,500 building a bridge you’ll never cross. And that’s just the bugs. We haven’t talked about onboarding time, cognitive overhead, or the fact that every new feature now has to navigate your defensive architecture.

The real kicker? That developer who has to add a field six months from now doesn’t care about your beautiful abstraction. They just want to ship their feature. Your complexity isn’t protecting them; it’s a roadblock they have to navigate.

Embrace Radical Simplicity

Start with the simplest thing that solves the problem. Not the simplest thing that might solve future problems. Not the simplest thing that shows off your design pattern knowledge. The simplest thing that solves THIS problem, RIGHT NOW.

In my 14 years at Amazon, I’ve always told my managers that my goal is to leave the company with a negative code contribution stat. That means I delivered value without adding complexity. Delete more than you write. Simplify more than you abstract. It’s a bit quippy, but the point stands: the best engineers aren’t measured by how much they build, but by how little code they need to solve the problem.

“But what about refactoring?” you ask. “Won’t we have to change it later?”

Yes. And refactoring 1,000 lines of straightforward code is infinitely easier than refactoring 5,000 lines of abstract spaghetti. Simple code is like working with clay. You can reshape it. Complex code is like working with concrete. Once it sets, good luck.

Clever Code is Bad Code

Here’s what I tell every team I work with: clever code is bad code.

You know what’s not clever? Code you can debug at 3 AM when production is down. When you’re stepping through the debugger, do you want to navigate through 10 classes, 3 interfaces, and a factory just to understand why a user can’t log in? Or do you want to see:

if user.password_matches?(password)
  log_them_in(user)
else
  show_error("Invalid password")
end

“But that’s too simple!” you say. Good. Simple means the junior developer can fix it. Simple means you can understand it six months from now. Simple means when you’re debugging, you’re solving the actual problem, not playing archaeology with your own cleverness.

Every abstraction layer is another step in the debugger. Another file to open. Another concept to hold in your head. Another place for bugs to hide.

Your Code Is Not a Framework

Here’s another trap: engineers who build application logic like they’re building the next Rails. Your business logic should look like imperative code. Because it is. It should read like a recipe, not like a framework tutorial.

# This is application code:
def process_order(order)
  validate_inventory(order)
  charge_customer(order)
  ship_items(order)
  send_confirmation(order)
end

# This is framework cosplay:
class OrderProcessor < AbstractProcessor
  include Validatable
  include Chargeable
  include Shippable

  register_handler :process

  before_action :validate
  after_action :notify

  # ... 200 more lines of ceremony
end

The first one I can understand in five seconds. The second one? I need a map, a compass, and possibly a sherpa.

The Decision You’re Already Making

Every time you sit down to code, you’re making a decision about complexity. Whether you realize it or not, you’re choosing somewhere on the continuum between “quick hack” and “enterprise architect’s fever dream.”

Make that decision intentionally.

Ask yourself: How much complexity am I willing to maintain? Is my goal to ship something that works? Or am I using this as an excuse to learn that new pattern I read about?

Both answers are valid. Sometimes you need to ship. Sometimes you need to learn. But be honest about which one you’re doing.

When You Actually Need the Bridge

Look, sometimes you do need complexity. If you’re processing payments, you need those abstractions. If you’re Netflix, microservices make sense. If you’re building a public API that thousands of developers depend on, yes, think about backwards compatibility.

The difference is those teams are solving actual problems, not theoretical ones.

Here’s the test: Can you point to a specific, current pain that this complexity solves? Not a future pain. Not a maybe pain. An actual, right-now, keeping-you-up-at-night pain.

No? Then you don’t need the bridge.

Cross the Street, Not the Ocean

Complexity is a bridge. Sometimes you need a massive suspension bridge to cross an ocean. But most of the time? You just need to cross the street.

Don’t build the Golden Gate Bridge when a crosswalk will do.

The best code is the code you don’t write. The second best is the simplest code that could possibly work. Everything else is just complexity looking for a problem to solve.