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

ActiveRecord Query Patterns That Actually Scale

ActiveRecord makes simple queries trivial and complex queries dangerous. These are the patterns that remain correct under load — and the common ones that quietly fall apart at scale.

Read more

Java Code Quality in Practice — The Rules That Help and the Ones That Don't

Most Java code quality guidance is either too abstract to apply or applied too rigidly to improve real codebases. Here is a honest assessment of the rules that consistently improve maintainability and the ones that create friction without payoff.

Read more

Wise, Bolt and Pipedrive Are Built in Tallinn — and They Hired the Backend Developers You Need

Estonia produced some of Europe's most successful tech companies. Those companies hired the engineers first and built retention structures to keep them.

Read more

What Actually Happens When Spring Boot Starts Up

Spring Boot startup involves auto-configuration, bean registration, context refresh, and lifecycle callbacks — in a specific order that determines when your code runs and why some startup bugs are hard to diagnose.

Read more