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.