# DRY, KISS, YAGNI & CQS

> Use coupling and cohesion to judge a design, and apply DRY, KISS, YAGNI, command–query separation and the Law of Demeter without overdoing them.

Software Engineer roadmap · Code that lasts · 17 min read · https://levelcode.org/roadmaps/software-engineer/design-principles

Design principles are compressed experience: each one names a mistake that teams kept making until someone gave it an acronym. Applied with judgment they keep code simple; applied as laws they produce abstract, over-engineered code that is harder to change than what it replaced. This lesson starts with the two forces underneath every principle — coupling and cohesion — then covers DRY, KISS, YAGNI, command–query separation and the Law of Demeter, including where each one stops helping.

## Coupling and cohesion

Every design principle is ultimately about making change cheap. Two measures predict how cheap change will be:

- **Coupling** is how much one part of the code depends on another's internals. If changing the `orders` table means editing the email templates, the invoicing job and three API handlers, those parts are tightly coupled.
- **Cohesion** is how strongly the things inside one module belong together. A `billing` package containing invoices, tax rules and payment retries is cohesive; a `utils` package with date formatting, HTTP retries and a slug generator is not.

The goal is **low coupling, high cohesion**: things that change together live together, and things that change for different reasons are separated by narrow interfaces. Keep this pair in mind as you read the rest of the lesson — each principle is a specific way to lower coupling or raise cohesion. The [coupling and cohesion note](https://levelcode.org/concepts/coupling-and-cohesion) lists the common kinds of coupling from worst to best.

## DRY: one source of truth for each piece of knowledge

**Don't Repeat Yourself** is usually quoted as "don't duplicate code", but its original statement is about **knowledge**: every piece of knowledge should have a single, authoritative representation in the system. The difference matters.

This is real duplication — one business rule written twice:

```go
// in the checkout handler
if order.Total >= 500_000 {
	order.ShippingFee = 0
}

// in the cart preview, written by someone else
if cart.Subtotal() >= 500_000 {
	preview.FreeShipping = true
}
```

When the free-shipping threshold changes, someone will update one and miss the other, and the cart will promise what checkout doesn't deliver. Give the rule one home:

```go
const freeShippingThreshold = 500_000 // in rupiah

func QualifiesForFreeShipping(subtotal int64) bool {
	return subtotal >= freeShippingThreshold
}
```

This is *not* real duplication — two pieces of code that look alike but represent different knowledge:

```go
func ValidateUsername(s string) error { return checkLength(s, 3, 30) }
func ValidateProductSKU(s string) error { return checkLength(s, 3, 30) }
```

Today both happen to allow 3–30 characters. If you merge them into one `ValidateShortCode`, the day marketing wants 40-character SKUs you must either break usernames or untangle the abstraction. Code that is **coincidentally** similar should stay separate; code that is **essentially** the same rule should be merged.

Two heuristics keep DRY honest:

- **The rule of three.** Tolerate the second copy; extract on the third, when you can see what actually varies.
- **Duplication is cheaper than the wrong abstraction.** A shared helper that has grown five boolean parameters to serve five callers is worse than five short copies. When that happens, inline it back into the callers and start again.

## KISS: keep it simple

**Keep It Simple** says that the simplest design that meets the requirements is the best one. Simple doesn't mean easy or short; it means few moving parts and few concepts a reader must hold at once. Complexity sneaks in as:

- Generic frameworks for a problem with one case ("a pluggable pipeline engine" for one CSV import).
- Clever code: a one-line chain of `map`/`reduce`/bitwise tricks that takes ten minutes to verify.
- Indirection without payoff: an interface with one implementation and no test that needs a fake, a factory that builds one type.

```ts
// Clever
const active = Object.fromEntries(Object.entries(users).filter(([, u]) => u.lastSeen > cutoff).map(([k, u]) => [k, { ...u, active: true }]))

// Simple
const active: Record<string, User & { active: true }> = {}
for (const [id, user] of Object.entries(users)) {
  if (user.lastSeen > cutoff) active[id] = { ...user, active: true }
}
```

The second version is longer and obviously correct. Prefer obvious.

## YAGNI: you aren't gonna need it

**YAGNI** says: don't build a capability until you actually need it. Speculative features cost you three times — the time to build them, the complexity everyone carries while they sit unused, and the rework when the real requirement turns out different from the guess (it usually does).

Typical YAGNI violations: a plugin system before the second plugin exists, configuration options nobody asked for, multi-tenant support "in case we sell to enterprises", a message queue for a job that runs once a night.

YAGNI is about *features and speculative generality*, not about quality. It does **not** mean skipping tests, error handling, or a clean structure that makes later change cheap. In fact, YAGNI only works *because* of those practices: you can afford not to build the plugin system today because good tests and modular code mean adding it later is cheap. The [YAGNI note](https://levelcode.org/concepts/yagni) and [when not to add infrastructure](https://levelcode.org/concepts/when-not-to-add-infrastructure) apply the same idea at the code and system level.

## Command–query separation

**Command–query separation** (CQS), from Bertrand Meyer, says every method should be either:

- a **query**: returns information and changes nothing, or
- a **command**: changes state and returns nothing (apart from an error).

Asking a question shouldn't change the answer. Violations create surprises:

```go
// Violates CQS: looks like a question, but marks the token as used
func (s *TokenStore) IsValid(token string) bool {
	t, ok := s.tokens[token]
	if !ok || t.Used {
		return false
	}
	t.Used = true // side effect hidden in a query
	s.tokens[token] = t
	return true
}
```

A teammate adds a log line `log.Printf("token valid: %v", store.IsValid(tok))` before the real check, and every login now fails. Separate the query from the command:

```go
func (s *TokenStore) IsValid(token string) bool {
	t, ok := s.tokens[token]
	return ok && !t.Used
}

func (s *TokenStore) MarkUsed(token string) error {
	t, ok := s.tokens[token]
	if !ok {
		return ErrUnknownToken
	}
	t.Used = true
	s.tokens[token] = t
	return nil
}
```

Queries can now be called freely — in logs, in tests, twice — without fear.

There are pragmatic exceptions where combining is the *correct* design, usually for atomicity: a stack's `Pop` returns and removes; `UPDATE ... RETURNING` in SQL changes rows and reports them; an atomic compare-and-swap or "consume this token" must check and change in one step, or two concurrent requests could both pass the check. When you combine, name the method as a command (`Consume`, `Pop`, `Claim`) so nobody mistakes it for a harmless question.

**CQRS** (command query *responsibility* segregation) takes the idea from methods to architecture: separate models — sometimes separate databases — for writes and reads. It's a much bigger commitment with real costs; the [event sourcing and CQRS note](https://levelcode.org/concepts/event-sourcing-cqrs) covers when it pays off.

## The Law of Demeter

The **Law of Demeter** ("talk only to your immediate friends") says a method should only call methods on its own fields, its parameters, and objects it creates — not on objects returned by other calls. Its smell is the train wreck:

```go
if order.Customer().Account().Wallet().Balance() >= order.Total() { ... }
```

This line knows that orders have customers, customers have accounts, and accounts have wallets. Restructure any of those and this code breaks. Ask instead of reaching:

```go
if order.Customer().CanAfford(order.Total()) { ... }
```

Now only `Customer` knows how affordability is computed. The law applies to objects with behaviour, not to plain data: walking a decoded JSON config (`cfg.Database.Pool.MaxConns`) or a fluent builder isn't a violation, because there's no hidden behaviour to couple to.

## Principles in tension

Principles pull against each other, and judgment is choosing between them in context:

| Tension | How to resolve it |
| --- | --- |
| DRY vs KISS | Extract only real shared knowledge; a little duplication beats a contorted helper. |
| YAGNI vs extensibility | Build for today's cases, but keep the code modular and tested so tomorrow's is cheap. |
| CQS vs atomicity | Separate by default; combine when check-then-act must be atomic, and name it as a command. |

## Practice

Review this Go code. Identify which principles it violates and rewrite it.

```go
type Inventory struct{ stock map[string]int }

// Reserve checks stock for a SKU and reserves qty units if available.
func (inv *Inventory) HasStock(sku string, qty int, reserve bool) bool {
	if inv.stock[sku] >= qty {
		if reserve {
			inv.stock[sku] -= qty
		}
		return true
	}
	return false
}
```

**Solution**

Problems:

- **CQS:** a method named like a query (`HasStock`) can change state.
- **Boolean flag parameter:** `reserve` makes the function do two different things, and `HasStock("A1", 2, true)` is unreadable at the call site.
- The doc comment names a different function than the one it documents — a sign the code changed and the comment didn't.

Split it into a query and a command. Because "check, then reserve" must not be interleaved with another reservation, the command does its own check and reports failure as an error rather than relying on the caller to call `HasStock` first:

```go
var ErrInsufficientStock = errors.New("insufficient stock")

type Inventory struct{ stock map[string]int }

// Available reports how many units of sku are in stock.
func (inv *Inventory) Available(sku string) int {
	return inv.stock[sku]
}

// Reserve takes qty units of sku out of stock, or returns
// ErrInsufficientStock and changes nothing.
func (inv *Inventory) Reserve(sku string, qty int) error {
	if inv.stock[sku] < qty {
		return ErrInsufficientStock
	}
	inv.stock[sku] -= qty
	return nil
}
```

If `Inventory` is used from several goroutines, `Reserve` also needs a mutex so the check and the update happen as one step — the same atomicity reason a database would use `UPDATE ... WHERE stock >= $1`.

## Key takeaways

- Aim for low coupling and high cohesion: things that change together live together.
- DRY is about knowledge, not text: merge copies of the same rule, keep coincidentally similar code apart, and prefer duplication to the wrong abstraction.
- KISS: the fewest moving parts and concepts that meet the requirement; obvious beats clever.
- YAGNI: don't build speculative features — but keep the tests and structure that make adding them later cheap.
- CQS: queries return answers without side effects; commands change state. Combine only for atomicity, and name it as a command.
- The Law of Demeter: ask an object to do something rather than reaching through its internals.

## Quiz

1. Two validation functions currently both allow 3 to 30 characters: one for usernames, one for product SKUs. What does DRY say?

   A. Keep them separate: they represent different rules that only coincidentally match today
   B. Merge them immediately into one function
   C. Move both into a utils package
   D. Delete one and call the other

   **Answer: A.** DRY is about duplicated knowledge. The username and SKU limits will change for different reasons; merging them couples unrelated rules.

2. Which method follows command–query separation?

   A. func (c *Cache) Get(key string) string — which also refreshes the entry's TTL and evicts stale entries
   B. func (q *Queue) Size() int — which returns the number of items
   C. func (s *Session) IsExpired() bool — which deletes the session if it is expired
   D. func (u *User) Name() string — which records an audit log entry

   **Answer: B.** Size only reports state. The others are queries with hidden side effects, so calling them in a log or a test changes the program's behaviour.

3. A teammate argues that YAGNI means you should skip writing tests until a bug appears. What is wrong with that?

   A. Nothing; tests are a speculative feature
   B. YAGNI only applies to frontend code
   C. YAGNI targets speculative features and generality; tests and clean structure are what make deferring features safe
   D. YAGNI requires 100% test coverage

   **Answer: C.** YAGNI works because changing code later is cheap. Tests and modular design are what keep it cheap, so they are part of the practice, not things it lets you skip.

4. Which line is the clearest Law of Demeter violation?

   A. cfg.Database.Pool.MaxConns, reading a decoded configuration struct
   B. user.CanAfford(price)
   C. strings.NewReplacer("a", "b").Replace(s)
   D. invoice.Customer().Address().Country().TaxRate()

   **Answer: D.** The invoice code reaches through three objects' internals to compute a tax rate, so it breaks if any of them is restructured. Plain data like a config struct carries no hidden behaviour to couple to.
