Principles of Good Design

Expert · 18 min read · ▶ live playground · ✦ checkpoint

Good JavaScript design is not about reciting SOLID, DRY, and YAGNI like rules from a wall poster. It is about making code easier to change for the next real requirement without building a temple for requirements you only imagined.

In Design Patterns in JavaScript, you learned to name recurring shapes. This lesson gives those shapes taste: when to extract, when to leave duplication alone, when to compose behavior, and how to draw module edges that other developers can understand.

Principles are pressure gauges

Design principles are useful when they help you notice pressure in the code:

  • SOLID asks whether responsibilities, substitutions, interfaces, and dependencies are honest.
  • DRY asks whether the same knowledge is repeated in several places.
  • YAGNI asks whether you are paying complexity now for a feature you do not need yet.

The danger is treating any of them as a one-word verdict. "DRY" can produce a helper so abstract that nobody can read it. "YAGNI" can become an excuse to ignore the obvious next change. "SOLID" can become class-shaped ceremony in a language where functions, objects, modules, and callbacks already give you excellent tools.

Here is the JavaScript translation of SOLID, without the drama:

  • Single responsibility: a function or module should have one clear reason to change.
  • Open/closed: when variation is real, add a new strategy/callback/table entry instead of editing a giant conditional forever.
  • Liskov substitution: if you use inheritance, subclasses must keep the base promise. If every subclass needs "except when..." comments, revisit Inheritance vs. Composition.
  • Interface segregation: pass the small thing a function needs, not a whole god object.
  • Dependency inversion: business logic should depend on a small capability, such as saveOrder(order) or calculateShipping(order), not on a concrete database, DOM node, or global service.

Notice the theme: principles help you keep promises small and honest.

DRY: duplicate knowledge, not every line

The best version of DRY is "do not repeat a decision." If the minimum title length is a product rule, that number should live in one place. If two functions happen to both use .map(...) today, that is not automatically a design problem.

const titleRules = {
  minLength: 3,
  maxLength: 80,
};
 
function isValidTitle(title) {
  const length = title.trim().length;
  return length >= titleRules.minLength && length <= titleRules.maxLength;
}
 
function titleHelpText() {
  return `Use ${titleRules.minLength}-${titleRules.maxLength} characters.`;
}
 
console.log(isValidTitle("JS"));
console.log(titleHelpText());

false
Use 3-80 characters.

That extraction is worth it because the repeated thing is knowledge: the rule itself. If product changes the limit to 100 characters, one object changes and both validation and help text stay aligned.

Now compare that with two similar-looking pieces of code that may deserve to stay separate:

function visibleProjectNames(projects) {
  return projects
    .filter((project) => project.archived === false)
    .map((project) => project.name);
}
 
function expensiveLineItemNames(items) {
  return items
    .filter((item) => item.cents > 5000)
    .map((item) => item.name);
}
 
console.log(visibleProjectNames([{ name: "Docs", archived: false }]).join(", "));
console.log(expensiveLineItemNames([{ name: "Monitor", cents: 25000 }]).join(", "));

Docs
Monitor

Both functions filter and map, but they express different business questions. A helper named filterThenMapByPredicateAndSelector would remove repeated syntax while hiding meaning. Good DRY removes repeated decisions. Bad DRY removes useful names.

Try it — compose a quote instead of predicting the future

This playground starts with a small order-quoting design. It is flexible where the requirements are already real: tax and shipping vary. It is deliberately boring everywhere else.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

This is not a grand architecture. It is a few small promises:

  • subtotalCents only totals items.
  • quoteOrder coordinates the quote.
  • taxRateForRegion answers one tax question.
  • shippingForMethod chooses one shipping strategy.

That is composition over inheritance. No BaseOrder, ExpressOrder, TaxableExpressOrder, or CaliforniaTaxableExpressOrder is needed. The quote workflow receives small capabilities and combines them. If a new overnight shipping method appears, add one strategy. If tax moves to a server API later, replace taxRateForRegion with a function that has the same promise. You already practiced this style in modules in ES Modules in Depth; the file boundary just makes the same promise more formal.

YAGNI: leave room without building rooms

YAGNI means "you aren't gonna need it." The useful version is restraint: do not build plugins, inheritance trees, cache layers, event buses, configuration languages, or generic factories before the code has earned them.

YAGNI does not mean "write messy code until pain arrives." The playground still has names, small functions, and clean inputs. It just refuses to prebuild imaginary features: multi-currency pricing, coupon stacking, warehouse routing, audit trails, and database persistence are not in the code because they are not in the current problem.

A good design leaves room. It does not build empty rooms. Passing taxRateForRegion into createOrderQuoter is room: the tax source can change. Building a full TaxProviderPluginRegistryFactory on day one is an empty room.

Tests help you keep this honest. In Testing Fundamentals, tests were executable examples. Here, they are also design pressure: if a function is hard to test without a real network, real clock, real DOM, and real database, it probably knows too much.

Small modules with sharp edges

A small module has a clear public surface and private mess inside. "Sharp edges" means callers know exactly what goes in, what comes out, and what errors or failure values to expect.

For a JavaScript module, ask:

  • What does this file export?
  • Which helpers can stay unexported?
  • Does this module mix pure calculation with storage, network, DOM, logging, and config?
  • Can a test call the core behavior without setting up the whole app?
  • Would a reviewer in Code Review & Working on a Team understand the change from the diff alone?

The best boundary is often boring: parseConfig(raw), quoteOrder(order), saveDraft(draft), renderTaskList(state). Each name says the job. Each module owns a small slice. The rest of the app does not need to know how the work happens.

This matters even more with AI assistance. In Using AI Responsibly, you learned that generated code is a draft you must verify. Small modules make that verification possible. A 40-line function with clear inputs can be reviewed, tested, and replaced. A 900-line "manager" that touches everything turns review into guesswork.

Naming is design

Names are not decoration. A good name lets the reader skip rereading the implementation every time.

Use names that say what role the value plays:

  • subtotalCents, not num
  • taxRateForRegion, not handle
  • visibleProjectNames, not getData
  • saveDraft, not process
  • shippingByMethod, not map

Prefer names from the domain over names from the implementation. paidOrders is usually better than filteredOrders because the important fact is the business meaning, not that .filter(...) was used. quoteOrder is better than calculate because it names the product action.

Also name the level honestly. If a function validates, call it isValidTitle or validateTitle. If it mutates state, do not disguise it as getUpdatedState unless it returns a new value. If a function writes to storage, use a verb like save, write, or persist. The name should prepare the reader for side effects.

Design principles do not make decisions for you. They slow you down just enough to ask better questions: what is repeated, what varies, what can wait, what boundary is sharp, and what name tells the truth?

Next, we finish the course by looking outward: reading real source code, joining professional codebases without fear, and turning this body of work into a portfolio you can explain.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion