📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What does KISS mean in practice for a .NET engineer, and how do you tell simple from merely easy?

Keep It Simple: essential versus accidental complexity, simple versus easy, readable code over clever code, and how to argue for simplicity in design reviews.

Mid-Level Theory Intermediate design-principles kiss maintainability

🎯 Key points

  • Prefer the simplest design that meets today's real requirements
  • Essential complexity comes from the problem; accidental complexity comes from our choices
  • Simple (few interleaved concepts) differs from easy (familiar or quick to type)
  • Clever one-liners and deep generic hierarchies hurt readability
  • Measure simplicity by how quickly a new team member can change the code safely
  • Simplicity often needs more thinking, not less

⏱ 30-second answer

KISS (Keep It Simple) means choosing the simplest design that meets the actual requirements, so the code is easy to read, test and change. The useful distinction is between essential complexity (the problem really is hard, such as tax rules) and accidental complexity (layers, patterns and frameworks we added). I also separate simple (few interleaved concepts) from easy (familiar or quick to write): copying a framework template is easy but may not be simple. In practice it means a plain class before an abstraction, a direct EF Core query before a generic repository, and readable code over clever LINQ.

🗣 Full answer (senior)

How I would answer this in an interview

"KISS means choosing the simplest design that meets the real requirements, so the next person can read, test and change it safely. Two distinctions make it practical. The first is essential vs accidental complexity (Fred Brooks): tax law or settlement rules are essentially complex and the code has to reflect that, while layers, patterns and frameworks we add are accidental complexity, and that's the part KISS attacks. The second is simple vs easy (Rich Hickey): simple means few concepts interleaved, one role per thing; easy means familiar or quick to start. Copying a 'clean architecture' template with a mediator, generic repositories and CQRS is easy, but for a six-endpoint admin API it isn't simple. In .NET that usually means a plain class before an abstraction, DbContext before a generic repository, minimal APIs or a service before a mediator, a modular monolith before microservices, and a readable loop before clever LINQ. Simplicity still needs structure: a 3,000-line Program.cs is easy, not simple. I add structure when a concrete need appears, and in reviews I argue with costs: files touched per change, onboarding time, test setup size."


1. Essential vs accidental complexity

Essential (keep, model clearly) Accidental (remove where it doesn't pay)
Tax rules, pricing, eligibility Layers that only forward calls
Regulatory workflows Patterns adopted by habit (mediator, CQRS, event bus)
Concurrency inherent in the domain Distributed systems where one process would do
Real integration constraints Custom frameworks, base classes, generic "engines"

2. Simple vs easy

Simple Easy
About The artefact: few interleaved concepts The person: familiar, near to hand
Example A handler calling DbContext A template with 12 projects you've used before
Long-term cost Low Can be high (ceremony, indirection)
Question How many things must I hold in mind to change this? How quickly can I start?

3. Common sources of accidental complexity in .NET

Source Simpler alternative
Generic repository + unit of work over EF Core DbContext directly, or focused query classes
Mediator for a handful of endpoints Minimal API handlers or a service class
Microservices for one small team Modular monolith with clear module boundaries
CQRS with separate databases for simple CRUD One model, maybe separate read queries
Custom base controllers/services with hooks Composition, filters, middleware
AutoMapper for 3 DTOs Explicit mapping methods
Deep generic hierarchies (BaseService<TEntity, TKey, TDto, ...>) Concrete classes
Clever LINQ with side effects A clear loop with named variables

4. How I apply it

  1. Write the most direct correct solution.
  2. Add structure when a specific pressure appears: hard-to-write tests, repeated change in one place, measured performance need, team boundaries.
  3. In design reviews, ask "what problem does this layer solve today?" and "what does it cost per change?"
  4. Measure simplicity by how quickly a new team member can make a safe change.

Production example

I inherited an [internal admin API] with [six] endpoints built on CQRS with separate read/write stores, a mediator, a generic repository with unit of work, and an event bus. Onboarding took [three weeks], adding a single field touched [eight] files, and people regularly couldn't find where logic lived. I listed every layer against the problem it actually solved for this app: most solved none. We collapsed it to minimal API endpoints using EF Core directly, keeping one service class where real business rules lived. The code shrank by [about 40%], change lead time went from days to hours, and we added a rule that architecture decision records must state the concrete problem a new layer solves.

🔍 What the interviewer is checking

Checks whether the candidate can resist over-engineering and articulate why.

Green flags

  • Distinguishes essential from accidental complexity.
  • Gives concrete .NET examples of unnecessary layers.
  • Separates simple from easy.
  • Knows simplicity still needs structure.

Red flags

  • Treats KISS as "do the quickest thing".
  • Defends patterns by name without stating the problem they solve.

📚 Learn in depth

1. Origins

Idea Source
KISS Attributed to Kelly Johnson (Lockheed Skunk Works): designs should be repairable by an average mechanic in the field
Essential vs accidental complexity Fred Brooks, No Silver Bullet (1986)
Simple vs easy Rich Hickey, Simple Made Easy (2011)
"Make it work, make it right, make it fast" Kent Beck

Johnson's framing is useful: simplicity is about the people who maintain it under pressure, not elegance for its own sake.

2. Signals of excess complexity

Signal Typical cause
One behaviour change touches many files Too many layers / pass-through classes
New starters need a diagram to add an endpoint Framework ceremony
Many interfaces with one implementation Speculative abstraction
Reviewers can't predict behaviour Cleverness, hidden side effects, magic conventions
Debugging requires stepping through generic plumbing Custom frameworks
Tests need elaborate setup Tangled concerns

3. Simplicity is not absence of structure

Too little structure Appropriate Too much
One giant Program.cs / god class Feature folders/modules with clear names 12 projects, 5 layers per feature
Inline SQL everywhere DbContext with configurations Generic repository + specification framework
Copy-paste Small focused helpers Custom framework

4. Readability over cleverness

// Clever: one pass, but readers must decode tuple arithmetic
var r = xs.Aggregate((0, 0), (a, x) => x > 0 ? (a.Item1 + x, a.Item2) : (a.Item1, a.Item2 + 1));

// Simple: two passes, obvious intent; optimise only if a benchmark says so
var positiveTotal = xs.Where(x => x > 0).Sum();
var nonPositiveCount = xs.Count(x => x <= 0);

5. Simplicity at scale

In large systems, KISS shows up as:

  • Fewer moving parts: one deployable until team or scaling needs force a split.
  • Boring technology: well-understood tools (SQL Server/PostgreSQL, built-in DI, ASP.NET Core) over novel ones without a reason.
  • Consistent patterns: one way to do common things across the codebase lowers cognitive load more than any individual "best" choice.
  • Clear module boundaries: complexity contained inside modules rather than spread across them.

6. When simplicity loses

Some complexity is worth paying for: a resilience pipeline for a flaky dependency, an outbox for must-not-lose events, caching backed by measurements. The test is whether there's a concrete, current problem and evidence the complexity solves it.

7. Try it yourself

  1. Count the files touched to add one field end to end in a feature of your codebase.
  2. List each layer and the problem it solves today.
  3. Remove one pass-through layer in a branch and compare the diff size of a typical change.
  4. Rewrite a clever LINQ expression as readable code and ask a colleague which they prefer.
  5. Time how long a new team member takes to add a small endpoint; record the friction points.

💻 Code

// Over-engineered: a generic repository and unit of work wrapping EF Core,
// which already is a unit of work with repositories (DbSet<T>).
// public interface IRepository<T> { IQueryable<T> Query(); void Add(T e); }
// public interface IUnitOfWork { Task CommitAsync(); }

// Simple: a minimal API endpoint using the DbContext directly.
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AdminDb>(o => o.UseInMemoryDatabase("admin"));
var app = builder.Build();

app.MapGet("/users/{id:int}", async (int id, AdminDb db, CancellationToken ct) =>
    await db.Users.FindAsync([id], ct) is { } user ? Results.Ok(user) : Results.NotFound());

app.MapPost("/users", async (User user, AdminDb db, CancellationToken ct) =>
{
    db.Users.Add(user);
    await db.SaveChangesAsync(ct);
    return Results.Created($"/users/{user.Id}", user);
});

app.Run();

public sealed class User
{
    public int Id { get; set; }
    public required string Email { get; set; }
}

public sealed class AdminDb(DbContextOptions<AdminDb> options) : DbContext(options)
{
    public DbSet<User> Users => Set<User>();
}

Requires the Web SDK and Microsoft.EntityFrameworkCore.InMemory.

⚠ Common mistakes

1. Adopting architecture templates for small problems

What happens: CQRS, mediator, microservices for a small app. Why it is wrong: accidental complexity slows every change. Do instead: start direct; add structure when a specific need appears.

2. Confusing easy with simple

What happens: "we always use this template". Why it is wrong: familiarity doesn't reduce the concepts a change requires. Do instead: judge by how much a reader must hold in mind to change it.

3. Clever one-liners

What happens: dense LINQ, nested ternaries, tuple tricks. Why it is wrong: the next reader pays in comprehension and bugs. Do instead: code that reads like the explanation you'd give.

4. Using KISS to justify no structure

What happens: everything in one file or method. Why it is wrong: that's complex, just concentrated. Do instead: the smallest structure that keeps responsibilities clear.

5. Removing essential complexity

What happens: tax or settlement rules "simplified" by ignoring edge cases. Why it is wrong: the software becomes wrong. Do instead: model essential complexity explicitly and clearly.

6. Custom frameworks and base classes

What happens: a home-grown BaseService<T> engine for all features. Why it is wrong: every developer must learn it; debugging goes through plumbing. Do instead: plain classes and built-in framework features.

7. Defending patterns by name

What happens: "we need a repository because that's clean architecture". Why it is wrong: no stated problem, no way to judge value. Do instead: state the concrete problem each layer solves.

8. Premature optimisation

What happens: complex caching or pooling without measurements. Why it is wrong: complexity without proven benefit. Do instead: measure first, then optimise the hot path.

9. Inconsistent ways of doing the same thing

What happens: five patterns for data access in one codebase. Why it is wrong: cognitive load multiplies. Do instead: agree on one simple default and document exceptions.

10. Never revisiting complexity

What happens: layers added for a past need remain forever. Why it is wrong: the cost persists after the benefit disappears. Do instead: periodically remove layers that no longer earn their keep.

🏛 Senior insight: trade-offs

Simplicity is a senior skill because it requires understanding the problem well enough to leave things out. I defend simple designs in reviews with concrete costs: files touched per change, onboarding time and test setup size.

Proposal Question I ask
Add a generic repository What can it do that DbContext cannot?
Split into microservices Which team or scaling need forces this?
Add a mediator Which cross-cutting behaviour needs the pipeline?
Add an interface Is there a second implementation or a test seam need?
Clever optimisation Is there a benchmark showing the need?

🔗 Follow-ups the interviewer may ask

  1. When: Candidate removes layers built for the future Why: Moves to YAGNI
    How to answer

    YAGNI (You Aren't Gonna Need It, from Extreme Programming) says do not build a capability until you actually need it. Speculative features cost time to build, delay what is needed now, add code to maintain, and are often wrong when the real requirement arrives. YAGNI does not mean skipping tests or refactoring; it is those practices that make adding the feature later cheap. I make an exception for decisions that are expensive to reverse, such as public API contracts, persisted data formats and security, where some upfront design pays off.

  2. When: Candidate merges similar code to simplify Why: Checks DRY judgement
    How to answer

    DRY (from The Pragmatic Programmer) says every piece of knowledge should have one authoritative representation in the system. It is about rules, not text: the GST rate, the definition of an overdue invoice or a validation rule should live in one place. Two blocks of code that look the same but change for different reasons are accidental duplication, and merging them couples unrelated features. I remove true duplication of business knowledge quickly, and I tolerate similar-looking code until a real shared concept appears, usually around the third repetition.

  3. When: Candidate says simplicity still needs structure Why: Probes appropriate separation of concerns
    How to answer

    Separation of Concerns (Dijkstra's term) means each part of a system addresses one concern, so you can reason about and change it without understanding everything else. In an ASP.NET Core app the typical concerns are HTTP handling (routing, model binding, status codes), business rules, persistence, and cross-cutting concerns such as logging, authentication, validation and error handling. Cross-cutting concerns go into middleware, endpoint filters or decorators rather than every handler. Separation can be by technical layer or by feature (vertical slices), and over-separating is its own problem because one change then spans many files.

🛠 Real-world problems: how you handle them

Six-endpoint admin API with full CQRS stack
Situation
An [internal admin API] with [six] endpoints used CQRS, a mediator, a generic repository and an event bus.
Signals
Onboarding took [three weeks]; adding a field touched [eight] files; frequent confusion over where logic lived.
Investigation
Mapped each layer to the problem it solved; most solved none for this application.
Resolution
Collapsed to minimal APIs using EF Core directly, keeping one service class for real business logic.
Prevention
Architecture decision records must state the problem a new layer solves.

How to tell it (STAR)

I inherited a small admin API built with every pattern in the book, and a one-field change touched [eight] files. I listed each layer against the problem it actually solved and most solved nothing here. We collapsed it to minimal APIs and EF Core, the code shrank by about [40%], and changes went from days to hours.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me