KISS: Simplest for the Requirements You Have
The simplest design that satisfies the known requirements — which is a different thing from the smallest amount of code you can write by ignoring some of them.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Two designs, one obviously smaller. How do I tell whether the small one is simple or just leaving the hard cases to whoever hits them?
Finance wants to export the order list to a spreadsheet: order id, customer, product, quantity, amount, date. They open it in Excel, in Germany.
Join the fields with commas and the rows with newlines. Six lines, no dependency, done before lunch. KISS.
The first supplier product called "Bolt, 6mm" shifts every column after it for that row, and nothing errors. Finance reconciles the wrong numbers for a week before anyone notices, because the file opens perfectly.
- The first supplier product called "Bolt, 6mm" shifts every column after it for that row, and nothing errors. Finance reconciles the wrong numbers for a week before anyone notices, because the file opens perfectly.
- This is not a future requirement being deferred. Commas in product names exist in the database *today*, which makes it a known requirement that the design does not satisfy — the definition of simplistic rather than simple.
- The escaping rules get added later, in the same function, one incident at a time: commas, then quotes, then embedded newlines, then a BOM so Excel reads UTF-8. The result is a hand-rolled CSV writer that is worse than the library and nobody dares touch.
- Adding a dependency on a CSV library would have been three lines and a package entry, and it is *simpler* in the only sense that matters: fewer cases you are responsible for getting right.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Product names are free text entered by suppliers, and some contain commas, quotes and the occasional line break.
- Amounts are decimal and finance is in a locale where the decimal separator is a comma.
- This is an internal admin feature with maybe forty users and no external contract.
- Every row that opens in the spreadsheet has the same number of columns as the header, whatever the data contains.
- The amount finance reads equals the amount in the database — no rounding introduced by formatting.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- One function owns turning rows into a CSV byte stream, including quoting. Owning it means owning every case of the format, not the cases that have caused an incident so far.
- The report query owns what the columns mean; it must not also own formatting, or a locale change becomes a query change.
- Nobody owns "the spreadsheet opened correctly in Excel" unless someone tests it, which is the requirement everyone treats as somebody else's.
- The seam is between "what the report contains" and "how it is serialized". Keeping them apart is what makes the eventual XLSX request a new serializer rather than a new report.
- Simplicity is a property measured at a boundary. A module can be simple to use and internally intricate — that is what a good library is — and the KISS question is which side of the boundary you are asking about (Designing a Module Interface).
Simple, simplistic, and how to tell them apart in review
Both designs below are small. One of them is small because the problem is small; the other is small because it is not solving all of the problem. The distinguishing question is not about the code — it is a question about the data: is there anything true today that this does not handle?
Ask it as a list. The moment "product names can contain commas" is written down next to "six columns", the six-line version stops being defensible on simplicity grounds, because it does not satisfy the requirements it was compared against.
const csv = [header, ...rows]
.map(r => r.join(','))
.join('\n')
// "Bolt, 6mm" -> two columns
// no quoting, no escaping, no BOM
// fails silently: the file opens, the numbers are wrongimport { stringify } from 'csv-stringify/sync'
export function writeOrdersCsv(orders: OrderRow[], sep = ','): string {
return '\uFEFF' + stringify(orders, { header: true, delimiter: sep })
}
// quoting, embedded newlines and the Excel BOM are
// someone else's problem, and they have an RFCThe second version is not simpler because it is shorter — it is barely shorter. It is simpler because the set of cases this team is responsible for getting right went from "all of RFC 4180" to "call it with the right rows". The cost is one dependency with a narrow surface, which is a much smaller ongoing obligation than a format specification you are implementing incident by incident.
The simplicity that was paid for by someone else
The subtler version of this failure is an interface that looks simple because it handed the difficult cases to its callers. Nothing in the module is complicated; the complexity was exported, and it now appears in five call sites where each one solves it slightly differently.
This is worth naming because it passes review easily. The diff is small, the function is short, and the cost lands somewhere else entirely — which is exactly the shape of cost this domain exists to make visible.
looks like A function whose documentation contains warnings rather than parameters: "caller must ensure the list is sorted", "does not handle empty input", "escape the values before passing them". Grep the call sites and each one has a small block of preparation before the call, and they are not the same block.
suggests The module drew its boundary to protect its own simplicity rather than to hide a decision. The cases did not go away; they went from one place to five, which is the direction that makes a change expensive (Change Amplification).
fix Move the case into the module and delete the warning from the docstring. If that makes the module's interface wrong for one caller, that caller has a genuinely different requirement and deserves a second entry point rather than a shared one with a flag (Boolean Parameters).
Which requirements count as known
KISS and YAGNI meet here and pull in opposite directions, which is why teams that recite both still argue. KISS says satisfy everything known; YAGNI says build nothing for what is not. So the whole decision reduces to which side of that line a requirement is on — and the honest answer is that some requirements are known but unspoken.
The unspoken ones are where this goes wrong. Nobody wrote down "product names contain commas", "the file must open in Excel" or "amounts must not be reformatted", but all three are facts about today, discoverable in ten minutes by looking at the data and asking finance one question (The Requirements Nobody States).
- True today — must be satisfied. Calling it "edge case" does not move it into the future (The Requirements Nobody States).
- Discoverable in ten minutes — treat as true today. The cost of looking is smaller than the cost of one silent wrong export.
- About the future, cheap to retrofit — do not build it (YAGNI, With Its Bill Attached).
- About the future, catastrophic to retrofit — build the minimal version, and say in one line why this one is an exception (Decision Records).
How to build it
Most important first.
- List the requirements you actually know, including the ugly ones. "Product names contain commas" belongs on the list next to "six columns", because it is equally true today.
- Choose the smallest design that satisfies all of them. Smallest is measured in cases you are responsible for, not in lines you typed — importing a CSV writer removes cases from your ledger and adds one dependency to it (Do We Need a Package for This?).
- Push what you do not want to own onto something that owns it well. A format with an RFC and an implementation is not a place to demonstrate independence (Build, Library, SaaS or Managed Service).
- Where a requirement is genuinely unknown, do not satisfy it — that is YAGNI's territory and it is a different question (YAGNI, With Its Bill Attached).
- Keep the intricacy behind a name.
writeOrdersCsv(orders)is simple at the call site regardless of what quoting rules live inside it, and that is where the caller's simplicity is decided (Information Hiding).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Simplistic version, next change: "finance wants a semicolon separator for their locale" means editing a function that also does quoting badly, with no tests for the cases it half-handles. Every such change carries a risk of shifting a column in a file nobody validates.
- Simple version, next change: the same request is one configuration argument to the writer, because separator handling is a case the library already owns.
- The change that neither version makes cheap: adding a computed column that finance wants derived from three fields. That is a report change, and it costs the same either way — which is the honest limit of this move.
- Taking a dependency for a small job means an upgrade obligation, one more entry in the lockfile and a supply-chain surface that is somebody's job to watch, forever.
- Insisting that all known requirements be satisfied slows the first version, and for a throwaway export read once by the person who wrote it, the six-line version was genuinely correct (When Design Does Not Pay).
- "Simplest that satisfies the requirements" pushes the argument onto what counts as known, which is a judgement call and is where two reasonable engineers will still disagree.
What can go wrong
- KISS is used to refuse a dependency, and the team re-implements a specified format badly — the most common way this slogan produces harm.
- KISS is used to refuse a known requirement, which is not simplification but scope reduction that nobody agreed to.
- Overcorrection: a reporting framework with pluggable serializers and a column DSL, for one internal CSV with forty users (Over-Design and Under-Design).
- The simplistic version ships, works for months, and fails silently rather than loudly — the worst combination, because there is no incident to learn from until the damage is compounded (Debuggability by Design).
- A CSV library is a dependency with a narrow, stable, well-specified surface and effectively no upgrade risk — close to the best case for taking one on (Transitive Dependencies).
- The hand-rolled version has no package dependency and a dependency on your continued attention to a format specification, which is the more expensive of the two.
- The report path depends on the query, and the serializer depends on nothing. Keeping that direction is what makes a second output format cheap (Dependency Direction).
- "KISS means fewer lines." The six-line version has fewer lines and more cases you are responsible for. Lines are not the unit; unhandled cases are (Long Functions).
- "KISS means no dependencies." Sometimes the simplest correct design is three lines and an import. Refusing dependencies is a different principle with a different cost (Dependency Management).
- "KISS and YAGNI are the same rule." YAGNI is about requirements you do not have. KISS is about the ones you do. They pull in opposite directions exactly when a known requirement is inconvenient.
- "Simple for the user justifies anything inside." Only up to the point where the inside becomes something a team has to change. A magic interface over an unmaintainable implementation moved the cost, it did not remove it (Leaky Abstractions).
- primitive-obsession
Testing it, and how it ages
- One test with a product name containing a comma, a quote and a newline. This is a three-line test and it is the entire difference between the two designs.
- A round-trip test — write the file, parse it back with an independent parser, compare to the input rows. Round-trips catch escaping bugs that assertions on strings do not (Property-Based Testing).
- Do not test the library. Test that your rows reach it correctly, which is your boundary (What a Unit Is).
- Known requirements accumulate as the data does. A design that satisfied everything known in year one is simplistic in year three purely because the data got uglier, with no code having changed.
- The pressure that eventually breaks the simple version is a genuinely new requirement — XLSX with formatting, or a hundred-thousand-row export that has to stream — and at that point the serializer boundary is what makes it a contained change (Cost-Aware Interfaces).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThe distinction between satisfying the known requirements and ignoring some of them is not language- or paradigm-dependent — though languages with rich standard libraries make the simple option cheaper, so the temptation to hand-roll is strongest where the ecosystem is thinnest.
- LIFETIME-SPECIFICFor a one-off export that one person opens once and deletes, the six-line version is correct and this entire lesson is over-thinking. The argument applies to code that will be run by other people, repeatedly, where a silently shifted column has time to do damage.
- CONTESTEDThe strongest opposing position comes from the XP tradition: build the simplest thing that could possibly work and let reality tell you what was missing, because most of what a team calls a "known requirement" is speculation dressed up, and the feedback from a real broken export is more reliable and cheaper than the design conversation that would have prevented it. That argument is strong when failures are loud and cheap to fix; it is weak here precisely because this failure is silent, and the asymmetry between loud and silent failure is the thing that should decide.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Programming Languages & Runtime Internals — how expensive "just pull in a library" is depends entirely on the ecosystem's packaging and versioning story, which is why the same KISS advice reads differently in JavaScript, Go and C.