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

The Head Chef Analogy: Why Teams Without a Tech Lead Fail

Imagine walking into a busy kitchen with 10 cooks and no head chef. Food is being made—but no one agrees on how it should taste.

Read more

Abstractions Are Powerful Until They Hide Too Much

Abstractions reduce cognitive load by hiding irrelevant details. They become a liability when they hide details that are actually relevant — leaving developers confused about system behavior and unable to reason about failures.

Read more

Your Transactions Are Bigger Than They Need to Be

Oversized transactions are one of the most common sources of lock contention, replication lag, and autovacuum interference in production databases — and they are almost always fixable without changing business logic.

Read more

When Should You Actually Break Your Spring Boot App into Microservices

The decision to extract a microservice is an engineering tradeoff, not an architectural rite of passage. Here is how to tell the difference between a legitimate reason and a rationalization.

Read more