KISS Principle

Keep It Simple, Stupid (aka Keep It Simple & Straightforward)

Goal: Ship solutions that are easy to build, read, test, run, and change.

By design, this deck favors brevity—because: KISS.

Why KISS?

  • Fewer bugs and faster delivery
  • Lower cognitive load → easier onboarding
  • Cheaper maintenance over time
  • Better testability and observability
  • Simpler systems survive pivots and staff changes

Short History

  • Origin: early 1960s engineering culture (US military & aerospace)
  • Popular phrasing: “Keep it simple, stupid.”
  • Core idea: prefer straightforward designs that can be built and serviced reliably by typical teams—not specialists only

The Essence

  • Prefer the simplest design that works today
  • Optimize for clarity and changeability, not cleverness
  • Delay abstractions until they pay for themselves

Common Misconceptions

  • KISS ≠ minimal features — it’s about design clarity, not doing less of the required scope
  • KISS ≠ anti-architecture — it’s the right amount of architecture
  • Simple ≠ simplistic — simple code can be robust, typed, and well-factored
  • Fewer lines ≠ simpler — clarity beats golfed code
  • YAGNI – don’t build it until you need it
  • DRY – avoid duplication after it hurts
  • Occam’s Razor – prefer fewer assumptions
  • Cohesion & Coupling – keep things focused and loosely connected
  • Rule of Three – abstract after you see at least 3 concrete cases

Heuristics (Fast Checks)

  • Can a new teammate explain it back in one sentence?
  • Can you test it with a few fast unit tests?
  • Are there fewer than 2–3 levels of indirection?
  • Does the name match what it does (no surprises)?
  • Would you bet next week’s on-call on it?

Decision Filter

  1. Does it solve the user need now?
  2. Is it the simplest path that remains safe? (security, data, perf guardrails)
  3. Will this be easy to change next sprint?
  4. Can we delete it later without drama?

Red Flags (Smells)

  • Premature abstraction / frameworks for one use case
  • Over-configurability for unknown future needs
  • Inversion-of-control or metaprogramming where a function would do
  • Pattern cargo-culting (factories of factories…)
  • Wide implicit behavior (magic) instead of explicit parameters

Angular Example — Keep the Component Simple

// Before: over-abstracted state & effects for a simple toggle
export class SettingsComponent {
  constructor(private store: Store) {}
  enableExperimental() { this.store.dispatch(toggleExperimental()); }
}

// After: local, explicit, testable
export class SettingsComponent {
  experimentalEnabled = signal(false);
  enableExperimental() { this.experimentalEnabled.set(true); }
}
  • Use global state (ngrx) for shared, cross-cutting concerns only
  • Prefer signals/inputs/outputs for local UI state

Angular Example — Observable Pipelines

// Before: nested streams + custom operators
readonly data$ = combineLatest([a$, b$, c$]).pipe(
  switchMap(([a, b, c]) => this.api.load(a.id, b.key, c.value)),
  retry({ count: 3 }),
  shareReplay({ bufferSize: 1, refCount: true }),
);

// After: flatten and name things
readonly params$ = combineLatest([a$, b$, c$]);
readonly data$ = params$.pipe(
  switchMap(([a, b, c]) => this.api.load(a.id, b.key, c.value)),
  shareReplay(1),
);
  • Name intermediate values; avoid clever one-liners
  • Use shareReplay(1) in UI; avoid exotic configs unless needed

.NET Example — Simple Domain Service

// Before: static helpers + many flags
public static class PriceUtils {
    public static decimal Calc(decimal basePrice, bool tax, bool promo, bool round) { /* ... */ }
}

// After: focused service with clear types
public interface IPricingService { decimal Calculate(Order order); }

public sealed class PricingService : IPricingService {
    private readonly ITaxPolicy tax;
    private readonly IPromoEngine promo;
    public PricingService(ITaxPolicy tax, IPromoEngine promo) { this.tax = tax; this.promo = promo; }
    public decimal Calculate(Order order) =>
        promo.Apply(tax.Apply(order.BasePrice), order.CustomerTier);
}
  • Replace boolean flags with types and collaboration
  • Keep public APIs small and intention‑revealing

Data & DTOs — Be Explicit

// Instead of "Dictionary<string, object> payload"
public sealed record CheckoutRequest(
    Guid CartId,
    string Currency,
    decimal Amount
);
  • Schemas > bags of properties
  • Defaults > implicit magic

Tests Drive Simplicity

  • Write the cheapest tests that catch 80% of issues
  • Favor pure functions where possible
  • Test public behavior, not private wiring
  • If it’s hard to test, it’s rarely simple → refactor

Architecture: Thin Slices

  • Vertical slice first (UI → API → DB) with minimal paths
  • Add seams for observability (logs, metrics) early
  • Avoid upfront plugin systems or DSLs
  • Prefer composition over inheritance

When Complexity Is Justified

  • Clear, measured performance constraints
  • Regulatory/security requirements
  • Shared platform code used by many teams
  • Long-lived domain rules that won’t churn

Even then, try a simple spike first.

KISS in PRs (Checklist)

  • Names are boring and precise
  • Function/class does one thing
  • No dead code or speculative hooks
  • Diff shows more deletions than additions when refactoring
  • README or ADR explains the choice in < 5 sentences

Anti‑Patterns to Avoid

  • Speculative Generality (designing for imaginary futures)
  • Patternitis (forcing patterns where none are needed)
  • God Objects / kitchen‑sink utilities
  • Magic Strings/Numbers instead of types

How To Start Simple (Team Habits)

  • Write a 1‑paragraph design sketch before coding
  • Agree on delete > generalize until the third use case
  • Keep a glossary of names; rename aggressively
  • Budget time for simplifying refactors each sprint

Tiny Case Study

Problem: Feature flagging one experiment

  • Don’t: adopt a platform and remote config for one flag
  • Do: if (env.EXPERIMENT_X === 'on') + UI toggle + log
  • Revisit after 3 flags or cross‑app needs

Metrics That Hint at Non‑KISS

  • High cyclomatic complexity
  • Unstable dependencies / deep graphs
  • Long build or feedback times
  • Frequent hotfixes around the same module

Communicating Simplicity

  • Prefer clear RFC/ADR over diagrams of boxes
  • Show before/after diffs and runtime impact
  • Document how to delete the feature later

The Payoff

  • Faster onboarding, safer changes
  • Happier on-call, fewer incidents
  • Roadmaps stay flexible

Simplicity compounds.

One‑Slide Summary

Build the simplest thing that meets today’s needs, is safe to ship, easy to test, and easy to change tomorrow. Defer abstraction until it clearly pays.

Appendix — Reading

  • A Philosophy of Software Design (Ousterhout)
  • Refactoring (Fowler)
  • Clean Architecture (Martin)
  • Designing Data‑Intensive Applications (Kleppmann)

Thank You

Q&A

Which part of our codebase feels least KISS today?