DRY, KISS and YAGNI are three of the most quoted principles in software development. They fit on a sticky note, they sound obviously right, and they get used in code reviews to win arguments. The trouble is that each one, applied blindly, can make code worse. DRY taken too far creates tangled abstractions. KISS gets used as an excuse to skip necessary design. YAGNI gets misread as "never plan ahead".
This guide explains what each principle actually protects you from, shows small before-and-after examples from Laravel and React code, and gives practical signals for when each one matters and when it does not.
The Short Version
- DRY (Don't Repeat Yourself): every piece of knowledge should have one authoritative place in the system.
- KISS (Keep It Simple): prefer the simplest design that solves today's real problem clearly.
- YAGNI (You Aren't Gonna Need It): do not build features or flexibility until there is a concrete need for them.
All three serve the same goal as most good design advice: keep the cost of the next change low. They just attack different ways that cost grows.
DRY: Duplicate Knowledge, Not Duplicate Lines
DRY comes from The Pragmatic Programmer by Andy Hunt and Dave Thomas, and the original wording is about knowledge, not text. Two blocks of code that look the same are not necessarily a DRY violation. Two places that encode the same business rule are, even if they look different.
When DRY really matters
Imagine an ERP where an invoice is "overdue" after 30 days. The rule lives in a Laravel query scope, in a React badge, in a nightly reminder job and in a report. Finance changes it to 45 days, someone updates three of the four places, and the dashboard now disagrees with the reminder emails.
// One home for the rule
class InvoicePolicy
{
public const OVERDUE_AFTER_DAYS = 45;
public static function isOverdue(Invoice $invoice): bool
{
return $invoice->unpaid()
&& $invoice->issued_at->lt(now()->subDays(self::OVERDUE_AFTER_DAYS));
}
}
// Query scope, job and report all use the same constant
public function scopeOverdue($query)
{
return $query->whereNull('paid_at')
->where('issued_at', '<', now()->subDays(InvoicePolicy::OVERDUE_AFTER_DAYS));
}For the frontend, expose the computed flag from the API (for example an is_overdue field on the invoice resource) instead of re-implementing the date math in React. Now the rule changes in one place.
When DRY hurts
The common failure is merging code that is similar by coincidence. Two forms both validate a phone number and an address, so someone builds a shared validateContact() helper. Six months later the supplier form needs a tax number and the customer form needs optional fields, and the helper sprouts flags:
// A "DRY" helper that now serves two masters
function validateContact(data, { isSupplier = false, requireTax = false,
allowEmptyPhone = false, legacyMode = false } = {}) {
// ...40 lines of branching
}Boolean flags that switch behaviour are a strong sign that two different concepts were forced together. Duplicating a few lines is usually cheaper than untangling the wrong abstraction later. A practical rule many teams use is the rule of three: tolerate duplication the first and second time, and extract only when a third copy appears and you can clearly name the shared concept. The post on when to create a new function, class, service or module goes deeper into choosing that boundary.

KISS: Simple Is Not the Same as Easy
KISS asks you to choose the design with the fewest moving parts that still solves the problem well. "Simple" means easy to understand and change, not "the first thing I typed" and not "the shortest code".
When KISS really matters
A classic case is reaching for infrastructure before it is needed. A team building an internal approval screen adds a message broker, an event-sourced store and a separate microservice. A database table with a status column, a Laravel queued job for notifications and a simple state check would have shipped in a week and handled the real load easily.
// Simple and explicit beats clever and generic
public function approve(PurchaseOrder $po, User $approver): void
{
abort_unless($po->status === 'pending', 422, 'Only pending orders can be approved.');
$po->update(['status' => 'approved', 'approved_by' => $approver->id]);
NotifyRequester::dispatch($po);
}Anyone on the team can read this in ten seconds, test it with one feature test and change it safely. If approvals later need multiple levels, you will add that design with real requirements in hand.
When KISS gets misused
KISS is not a reason to skip work that the problem genuinely requires. Putting everything in one 600-line controller is not simple; it is just unstructured. Skipping input validation, transactions or error handling is not simple either; it moves the complexity into production incidents. The useful question is: what is the simplest design that is still correct, safe and readable? The article on clean code vs clever code covers the same trade-off from another angle.
YAGNI: Don't Build for an Imaginary Future
YAGNI comes from Extreme Programming. It says: do not add functionality because you think you might need it. Every speculative feature costs time to build, adds code to read and test, and often turns out to be shaped wrong once the real requirement arrives.
When YAGNI really matters
Typical speculative work in business apps includes a plugin system nobody has asked for, multi-currency support for a single-market product, a generic "rules engine" for three fixed rules, or configuration options with exactly one value ever used.
// Speculative: a strategy registry for one shipping method
interface ShippingStrategy { calculate(order: Order): number }
const registry = new Map<string, ShippingStrategy>();
registry.set('flat', new FlatRateStrategy());
export const shippingFor = (o: Order) => registry.get(o.shippingType ?? 'flat')!.calculate(o);
// YAGNI version: what the business actually has today
export const FLAT_SHIPPING = 250;
export const shippingFor = (_o: Order) => FLAT_SHIPPING;When a second shipping method appears, refactoring a one-line function into a strategy takes minutes, and you will design it around the real second case instead of a guess.
Where YAGNI does not apply
YAGNI is about features and flexibility, not about basic engineering quality. Some decisions are expensive to reverse, and it is worth thinking about them early:
- Data model and IDs. Changing primary keys or splitting a core table after millions of rows is painful.
- Security and permissions. Retrofitting authorization into an ERP is far harder than designing roles from the start.
- Multi-tenancy. If you know you are building SaaS, tenant isolation is a day-one decision.
- Public API contracts. Once mobile apps or partners depend on an endpoint, changes need versioning.
The test is reversibility. If a decision is cheap to change later, defer it. If it is expensive to change later, give it some thought now, but still build only what you need.
When the Principles Pull in Different Directions
The three principles sometimes conflict. Removing duplication (DRY) can add an abstraction that makes the code less simple (KISS). Building an abstraction "so we don't repeat ourselves later" can be speculative work (YAGNI). A practical order of priority when they clash:
- Correctness first. One source of truth for business rules almost always wins, because inconsistent rules cause real bugs.
- Readability second. If removing duplicated code (not knowledge) makes things harder to read, keep the duplication.
- Defer flexibility. Add extension points when the second or third real use case arrives, not before.
These ideas complement the SOLID principles: SOLID tells you how to shape code that changes, while DRY, KISS and YAGNI help you decide how much structure to add in the first place.
A Quick Code Review Checklist
- Is the same business rule encoded in more than one place? Consolidate it. (DRY)
- Does a shared helper take boolean flags that change its behaviour? It may be two concepts. (DRY misuse)
- Could a new teammate explain this code after one read? If not, look for a simpler shape. (KISS)
- Is there infrastructure, configuration or abstraction with only one real use? Question it. (YAGNI)
- Is this decision expensive to reverse later, such as schema, security or API contracts? Then think it through now.
Conclusion
DRY, KISS and YAGNI are not laws; they are reminders about three common ways software gets expensive. Keep each piece of business knowledge in one place, choose the simplest design that is still correct, and build for the requirements you actually have. Apply them with judgement, especially when they pull against each other, and they will keep your Laravel, React and ERP codebases easier to change month after month.
