What Senior Developers Mean When They Say Keep It Simple

by Arif Ikhsanudin, Backend Developer

The Advice That Gets Misunderstood

"Keep it simple" is the most commonly given advice in software engineering and among the least commonly understood. Junior developers hear it as "don't write complex code" and come away writing naive implementations that don't handle edge cases. Mid-level developers hear it as "don't over-engineer" and hold back useful abstractions. Neither interpretation is what experienced engineers mean when they say it.

What they mean is: keep complexity local. Don't let it leak.

A complex algorithm that lives in one well-named function with clear inputs and outputs is simple in the sense that matters — it doesn't infect the rest of the system. A simple data structure that is accessed and mutated in twenty different places is not simple at all — its simplicity is an illusion that makes every bug harder to trace and every change more dangerous.

The Two Axes of Complexity

Rich Hickey's 2011 talk "Simple Made Easy" (Strange Loop conference) draws a useful distinction that engineers reference frequently. Simple means having one role, one concept, one task — not interleaved with other things. Easy means familiar, close at hand, convenient. These are different properties. A thing can be simple and hard (pure functional programming), easy and complex (mutable global state — so convenient, so tangled).

The advice "keep it simple" in experienced hands means: prefer the design that keeps concerns un-interleaved, even if that design is initially harder to use. A stateless service is simpler than a stateful one even though distributed state management is in some ways easier (it feels natural to cache results in memory). The stateless design keeps concerns separate; the stateful design entangles request handling with memory management with consistency guarantees.

What Complexity Leaking Looks Like

Complexity becomes a problem when it escapes its container. Concrete examples:

Business logic in the database: Stored procedures that encode business rules couple your data layer to your logic layer. Every service that touches that database now has invisible behavior dependencies. Adding a service in a different language is now complicated by logic that lives in SQL.

State assumptions across services: Service A returns an order in state PENDING. Service B, called afterward, assumes the order has already been validated by A. This assumption lives in no contract — it's informal shared knowledge. When A changes its validation rules, B breaks in ways that are not obvious.

Configuration that changes behavior at runtime: A feature flag that changes how a calculation works can be a powerful tool. Six feature flags that interact with each other can produce 2^6 = 64 behavioral states, most of which have never been tested.

In each case, the complexity exists — that's fine, complex systems need to handle complex requirements. The problem is that the complexity is spread across boundaries, making it impossible to understand any single component without understanding everything that touches it.

Practical Signals That Complexity Has Leaked

You can tell complexity has leaked when:

  • Changing one service requires coordinated changes to another service at the same time
  • You can't understand what a function does without reading its callers
  • A test for component A requires setting up the state of component B
  • The same business rule is enforced in multiple places and they've drifted out of sync
  • You can't run one part of the system locally without the entire system running

These are not signs that your codebase is complex — complex systems are fine. They're signs that the complexity is not contained.

What Simplicity Requires

Keeping things simple in the meaningful sense requires active decisions:

Clear ownership of behavior: Business logic lives in one layer. Validation happens at the entry point, not scattered through the call chain. The database stores and retrieves data; it doesn't compute business outcomes.

Explicit over implicit: State shared between components is documented and versioned. Assumptions between services are encoded in contracts (OpenAPI specs, Protobuf schemas, consumer-driven tests) rather than undocumented conventions.

Minimal surface area: The interface between components — what one module needs to know about another — should be as small as possible. The larger the interface, the more coupling there is, and the more places where changes in one component propagate to another.

// Wide interface: callerhas to understand the entire Order object
void processOrder(Order order);

// Narrow interface: caller only exposes what the method needs
void processOrder(String orderId, BigDecimal amount, Currency currency);

The narrow version is simpler even though it's more code at the call site. It makes the dependency explicit and limits the blast radius of changes to Order.

The Practical Takeaway

The next time you're asked to simplify something, ask: am I being asked to make this less complex, or to keep the complexity from spreading? If the answer is the latter, you're working on something real. Push for a design that localizes the complexity — not one that hides it or removes necessary detail. The goal is containment, not elimination.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Choosing a CI/CD Tool Is Less Important Than Having a Good Pipeline

Tool selection debates consume engineering time that would be better spent on the pipeline practices that actually determine delivery performance. The tool is a multiplier; if the pipeline is bad, a better tool makes a bad pipeline run faster.

Read more

Mocking Everything in Your Tests Is a Sign Something Is Wrong

Tests that mock every dependency are not unit tests — they are tests of mock configuration. When a test setup contains more mock declarations than real assertions, the test has stopped verifying behavior and started verifying wiring.

Read more

The Pull Request That Was Too Big to Review Properly

Large pull requests are one of the most reliable predictors of poor code review quality. The cognitive overhead of reviewing a 2,000-line change is high enough that reviewers inevitably skim — and the bugs they miss ship.

Read more

The Branching Strategy That Fits a Team of Two Will Break a Team of Ten

Branching strategies that work at small team sizes create coordination problems at larger ones — and the transition point is usually crossed before anyone notices, leaving the team with a workflow that serves nobody.

Read more