cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Structural & behavioral patterns

Learn decorator, proxy, adapter, facade, strategy, command, and middleware patterns through runnable JavaScript examples and undo redo flows.

By the end, you can
  • 01
    Wrap without rewritingUse decorators and proxies to add logging, caching, validation, access control, or lazy work around existing behavior.
  • 02
    Normalize and simplify APIsWrite adapters and facades that turn messy external shapes into one readable interface your app owns.
  • 03
    Model actions and algorithmsChoose strategy, command, and middleware when runtime decisions, undo history, or request pipelines need names.

Patterns as small shapes

Design patterns are names for recurring shapes in code. They are not rules to force into every file. They help a team say, “this wrapper adds behavior,” “this adapter protects our app from an outside API,” or “this command can be undone.”

Definition

Structural patterns organize relationships between objects, functions, and external interfaces. Behavioral patterns organize how work is selected, recorded, and passed along. In this lesson, the practical shapes are decorator, proxy, adapter, facade, strategy, command, and middleware.

Real-life analogyTravel adapters and a hotel concierge

When you travel, an adapter lets your charger fit a different socket. A concierge hides a complicated set of hotel steps behind one request. A security desk is a stand-in that controls access. The patterns in this lesson give code the same kinds of seams.

In real life: A plug adapter changes one plug shape into another
In JavaScript: An adapter changes one API shape into your internal shape
In real life: A concierge takes one request and coordinates several people
In JavaScript: A facade exposes one method over several subsystem calls
In real life: A security desk controls who can enter
In JavaScript: A proxy controls access to a target object

Where the analogy stops: Travel metaphors are fixed objects. JavaScript patterns are code you design, compose, test, and sometimes delete when they add more indirection than clarity.

Decorator, proxy, adapter, and facade at a glance
PatternPrimary jobTiny example
DecoratorWraps an object or function to add behavior before, after, or around the original call.withRetry(fetchJson) still calls fetchJson; it adds retry behavior.
ProxyStands in for a target and controls access to reads, writes, calls, or construction.new Proxy(settings, traps) can validate writes or lazily load values.
AdapterConverts one interface or response shape into another shape your app expects.fromWeatherApiB(raw) returns the same internal report shape as API A.
FacadeOffers one small API over a subsystem that has several steps or collaborators.media.play("intro") hides load, volume, analytics, and start calls.

This lesson links to nearby architecture lessons instead of duplicating them. Factories and modules belong in creational patterns. Event emitters belong in observer & pub/sub. Reducers and stores come next in state management, and project structure belongs in app architecture.

Decorator and proxy: wrap or stand in

WRAPPERS

A decorator adds behavior around an object or function without changing the original implementation. Function decorators are ordinary higher-order functions: withLogging(fn), withRetry(fn), and memoize(fn) all receive a function and return another function. The memoization lesson goes deeper on caching; the decorators lesson covers TC39 @decorator syntax. This section is about the pattern, not the syntax.

Function decorators: logging plus memoizationPop out in the code editor (opens in a new tab)JavaScript
function withLogging(fn) {  return function logged(...args) {    console.log("calling " + fn.name + "(" + args.join(", ") + ")");    const result = fn(...args);    console.log("returned " + result);    return result;  };} function withMemo(fn) {  const cache = new Map();  return function memoized(value) {    if (cache.has(value)) return cache.get(value);    const result = fn(value);    cache.set(value, result);    return result;  };} function priceWithTax(price) {  return Number((price * 1.1).toFixed(2));} const loggedPrice = withLogging(withMemo(priceWithTax));console.log(loggedPrice(20));console.log(loggedPrice(20));

Read it line by line. Line 1 creates a wrapper factory. Line 4 still calls the original function, so the business rule lives in one place. Line 10 creates a closure over a cache. Line 23 composes wrappers: logging sees the call, and memoization can reuse the previous result. Object decorators use the same idea with methods: return an object that forwards to the original object while adding timing, permissions, or tracing.

A proxy is different. It is a stand-in controlling access to a target. You can write a plain wrapper object, or use JavaScript’s built-in Proxy and Reflect APIs to intercept property reads and writes.

Plain wrapper object and built-in ProxyPop out in the code editor (opens in a new tab)JavaScript
const stock = { notebook: 3 }; const guardedStock = {  get(name) {    return name in stock ? stock[name] : 0;  },  set(name, value) {    if (value < 0) throw new Error("Stock cannot be negative");    stock[name] = value;  },}; guardedStock.set("pen", 2);console.log(guardedStock.get("pen"));console.log(guardedStock.get("missing")); const settings = new Proxy({ theme: "dark" }, {  get(target, key, receiver) {    console.log("read " + String(key));    return Reflect.get(target, key, receiver);  },  set(target, key, value, receiver) {    if (key === "theme" && !["dark", "light"].includes(value)) {      throw new Error("Unsupported theme");    }    return Reflect.set(target, key, value, receiver);  },}); console.log(settings.theme);settings.theme = "light";console.log(settings.theme);

The plain wrapper makes missing stock read as zero and blocks negative writes. The built-in proxy sees property access itself: get logs reads, set validates the theme, and Reflect.set performs the normal write after validation. Use this power for lazy loading, caching, validation, access control, or diagnostics; avoid it when a simple function call is easier to debug.

Adapter and facade: translate or simplify

BOUNDARIES

Adapter code lives at a boundary. It says, “the outside world speaks one shape; our app speaks another.” The goal is not cleverness. The goal is to prevent two weather APIs, payment APIs, SDKs, or legacy callbacks from leaking their details through the whole app.

Adapters for two weather APIs and one callback APIPop out in the code editor (opens in a new tab)JavaScript
function fromWeatherApiA(response) {  return { city: response.city, celsius: response.tempC };} function fromWeatherApiB(response) {  return {    city: response.location.name,    celsius: Math.round((response.current.tempF - 32) * 5 / 9),  };} function readLegacyProfile(id, callback) {  callback(null, { id, name: "Ada" });} function profilePromise(id) {  return new Promise((resolve, reject) => {    readLegacyProfile(id, (error, profile) => {      if (error) reject(error);      else resolve(profile);    });  });} const reports = [  fromWeatherApiA({ city: "Chennai", tempC: 31 }),  fromWeatherApiB({ location: { name: "London" }, current: { tempF: 59 } }),]; console.log(reports.map((report) => report.city + ":" + report.celsius).join(" | "));profilePromise(7).then((profile) => console.log(profile.name));

Lines 1 and 5 return the same internal shape even though the outside responses disagree. Line 16 is an adapter too: it turns a callback API into a promise so callers can use promisification and async/await style code.

A facade hides orchestration. The subsystem may need several calls, but most of your app should not repeat those calls in the right order.

A media.play() facade over several stepsPop out in the code editor (opens in a new tab)JavaScript
function createMediaFacade(video, analytics) {  return {    play(title) {      video.load(title);      video.setVolume(0.7);      analytics.track("play:" + title);      return video.start();    },  };} const video = {  load(title) { console.log("load " + title); },  setVolume(level) { console.log("volume " + level); },  start() { return "playing"; },};const analytics = { track(event) { console.log("track " + event); } }; const media = createMediaFacade(video, analytics);console.log(media.play("intro"));

Line 16 is the public API the rest of the app uses. Inside the facade, loading, volume setup, analytics, and starting playback can change without every caller changing. Facades also work well for fetch helpers that combine JSON parsing, error messages, authentication headers, and retry policies behind one call.

Strategy: choose an algorithm at runtime

INTERACTIVE

Strategy turns a family of algorithms into interchangeable functions. Instead of growing a long switch statement or if/else chain, you store named functions in a map and select one at runtime. Shipping cost, discounts, sorting, formatting, and validation are common examples.

Change the shipping strategy
Strategy map sourcePop out in the code editor (opens in a new tab)JavaScript
const shippingStrategies = {  standard(cart) {    return cart.subtotal >= 75 ? 0 : 7;  },  express(cart) {    return 12 + cart.items * 2;  },  pickup() {    return 0;  },}; function quoteShipping(cart, strategyName) {  return shippingStrategies[strategyName](cart);} const cart = { subtotal: 80, items: 2 };console.log(quoteShipping(cart, "standard"));console.log(quoteShipping(cart, "express"));console.log(quoteShipping(cart, "pickup"));
Quotestandard
cart subtotal$80
items2
shipping fee$0
Try it yourself

The selected key is standard, so quoteShipping(cart, "standard") calls that function from the strategy map.

The cart stays the same. Only the algorithm name changes, which is the point of strategy.

Line 1 names the family. Line 13 chooses the current strategy. The caller only passes a cart and a strategy name, so adding overnight shipping means adding a function rather than editing a central conditional. Keep a small conditional when there are only two stable choices; strategy pays off when choices grow, move into configuration, or need separate tests.

Command: actions as objects with undo and redo

STEP THROUGH

A command is an action object. It knows how to execute() and how to undo(). A manager does not need to know the editor details; it only stores commands in undo and redo stacks. This shape powers text editors, drawing apps, queues, macro recording, optimistic UI, audit logs, and retryable work.

Command history: execute, undo, redo, then branch
Step 0 of 21Ready
Your turn: follow the blue line

A command is an object with execute and undo. Step through a real editor history and watch undo, redo, and the redo-stack clearing rule.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  let text = initialText;  return {    getText: () => text,    insert: (value) => { text += value; },    removeEnd: (count) => { text = text.slice(0, -count); },  };} function insertText(editor, value) {  return {    execute() { editor.insert(value); },    undo() { editor.removeEnd(value.length); },    label: "insert " + JSON.stringify(value),  };} function createCommandManager() {  const undoStack = [];  const redoStack = [];  return {    run(command) {      command.execute();      undoStack.push(command);      redoStack.length = 0;    },    undo() {      const command = undoStack.pop();      if (!command) return;      command.undo();      redoStack.push(command);    },    redo() {      const command = redoStack.pop();      if (!command) return;      command.execute();      undoStack.push(command);    },    counts: () => ({ undo: undoStack.length, redo: redoStack.length }),  };} const editor = createEditor("Hi");const history = createCommandManager();history.run(insertText(editor, "!"));console.log(editor.getText(), JSON.stringify(history.counts()));history.undo();console.log(editor.getText(), JSON.stringify(history.counts()));history.redo();console.log(editor.getText(), JSON.stringify(history.counts()));history.undo();history.run(insertText(editor, "?"));console.log(editor.getText(), JSON.stringify(history.counts()));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the whole flow. The manager runs an insert command, undoes it, redoes it, undoes again, then runs a different command. The important rule is on line 25: a brand-new command clears the redo stack because the old future is no longer the user’s future.

Middleware: a chain that calls next()

ONION ORDER

Middleware is a chain of functions. Each function receives a context object and a next() function. Code before next() runs on the way in; code after next() runs on the way out. Express-style request handlers, Koa’s async onion, and Redux middleware all use this mental model.

Synchronous middleware runner
Sync compose middlewarePop out in the code editor (opens in a new tab)JavaScript
function compose(middleware) {  return (context) => {    let index = -1;    function dispatch(i) {      if (i <= index) throw new Error("next() called twice");      index = i;      const fn = middleware[i];      if (!fn) return;      return fn(context, () => dispatch(i + 1));    }    dispatch(0);    return context.log;  };} const stack = [  (ctx, next) => { ctx.log.push("auth before"); next(); ctx.log.push("auth after"); },  (ctx, next) => { ctx.log.push("timing before"); next(); ctx.log.push("timing after"); },  (ctx) => { ctx.log.push("handler"); },]; const app = compose(stack);console.log(app({ log: [] }).join(" -> "));
Outputonion order

auth before -> timing before -> handler -> timing after -> auth after

Try it yourself
Real output from the exact snippet

The synchronous runner calls next() directly. Work before next() runs inward; work after next() runs as each call returns.

This is the same shape Express-style middleware uses when work is synchronous.

The synchronous runner is a real compose function: it keeps an index so a middleware cannot call next() twice, then dispatches the next function in the array. The async version changes one thing: next() returns a promise, and each wrapper awaits it.

Async middleware onion with await next()
Step 0 of 13Ready
Your turn: follow the blue line

Middleware is a chain of functions. Step through the async version to see the onion order created by await next().

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  return async (context) => {    let index = -1;    async function dispatch(i) {      if (i <= index) throw new Error("next() called twice");      index = i;      const fn = middleware[i];      if (!fn) return;      await fn(context, () => dispatch(i + 1));    }    await dispatch(0);    return context.log;  };} const stack = [  async (ctx, next) => {    ctx.log.push("auth before");    await next();    ctx.log.push("auth after");  },  async (ctx, next) => {    ctx.log.push("timing before");    await next();    ctx.log.push("timing after");  },  async (ctx) => {    ctx.log.push("handler");  },]; async function main() {  const app = composeAsync(stack);  const log = await app({ log: [] });  console.log(log.join(" -> "));}main();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

That await next() is why the order feels like an onion. Auth starts outside, timing starts inside it, the handler is the center, then timing and auth resume after the awaited inner work resolves.

Choosing a pattern without over-building

Patterns overlap because real code overlaps. A decorator may use closures. A proxy may cache. A facade may call adapters. That is normal. Choose the name that explains the seam you are designing.

When each pattern earns the indirection
PatternFitUse it when
DecoratorUsefulRepeated cross-cutting behavior such as logging, retry, memoization, timing, or permissions around existing functions.
ProxyUseful, but powerfulAccess control, validation, lazy loading, and tracing where callers should not touch the target directly.
Adapter/facadeVery commonBoundary code near APIs, SDKs, browser subsystems, and legacy callback functions.
StrategyUse when choices changeShipping, discounts, sorting, validation, or formatting algorithms chosen at runtime.
CommandUse for historyUndo/redo, macro recording, queues, retries, audit logs, or UI actions that need names.
MiddlewareUse for pipelinesRequest handling, Redux-style action interception, logging, auth, timing, and plugin chains.
Small code shapes to classifyPop out in the code editor (opens in a new tab)JavaScript
const examples = [  "withRetry(fetchJson)",  "new Proxy(settings, traps)",  "fromWeatherApiB(response)",  "shippingStrategies.express(cart)",  "history.run(insertText(editor, '!'))",  "await logger(ctx, next)",]; console.log(examples.length);
Which pattern family is this?
  • const reliableFetch = withRetry(fetchJson);
  • new Proxy(settings, { set(target, key, value) { ... } })
  • fromWeatherApiB(rawResponse)
  • media.play("intro")
  • shippingStrategies[selected](cart)
  • history.run(insertText(editor, "!"))
Try it yourself
0 of 6 correct

Sort each card by the job the code is doing.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Composite, iterator, and chain of responsibility are close relatives worth recognizing. Composite treats individual objects and groups uniformly. Iterator standardizes walking a collection. Chain of responsibility passes a request along handlers until one handles it; middleware is often a practical chain with a shared context and next().

Common misconceptions

  • “Decorator means @decorator syntax.” The syntax is one language feature. The pattern is plain wrapping.
  • “Proxy always means the built-in Proxy.” A plain stand-in object can be the proxy pattern when it controls access.
  • “Adapter and facade are synonyms.” Adapter translates a shape; facade simplifies a subsystem.
  • “Strategy is always better than switch.” A local switch can be clearer until algorithms grow or become runtime configuration.
  • “Middleware is only for servers.” Redux-style state tools, client logging, feature flags, and analytics pipelines can use the same chain idea.
Similar-looking patterns, different jobs
ConfusionBetter questionLikely choice
Decorator vs proxyAm I adding behavior around a call, or controlling access to a target?Decorator for wrapping; proxy for access control.
Adapter vs facadeAm I translating an interface, or hiding several subsystem steps?Adapter for shape; facade for simplicity.
Strategy vs commandAm I choosing an algorithm now, or recording an action for later?Strategy for choice; command for history.
Middleware vs chain of responsibilityDoes each step share context and call next(), or does one handler stop the request?Middleware for pipelines; chain for handoff.

Practice exercises

Exercise 1 · Warm-upRead a decorator

Predict the final value printed by the decorator exercise.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function withLogging(fn) {
  return (...args) => {
    const result = fn(...args);
    console.log("result:" + result);
    return result;
  };
}
const shout = withLogging((word) => word.toUpperCase());
console.log(shout("pattern"));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeNormalize a payment response

    Run the adapter mentally and type the normalized output.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function adaptPaymentA(response) {
      return { id: response.payment_id, cents: response.amount_cents, ok: response.status === "paid" };
    }
    const payment = adaptPaymentA({ payment_id: "p1", amount_cents: 1299, status: "paid" });
    console.log(payment.id + ":" + payment.cents + ":" + payment.ok);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeChoose a discount strategy

      Predict the number printed by the selected strategy.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const discounts = {
        student: (total) => total * 0.8,
        none: (total) => total,
      };
      console.log(discounts.student(50));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeApply the redo-clearing rule

        Trace the undo and redo arrays after a different command runs.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function createEditor(text = "") {
          return {
            text,
            insert(value) { this.text += value; },
            removeEnd(count) { this.text = this.text.slice(0, -count); },
          };
        }
        function insertText(editor, value) {
          return {
            execute() { editor.insert(value); },
            undo() { editor.removeEnd(value.length); },
          };
        }
        const undo = [];
        const redo = [];
        const editor = createEditor("A");
        const command = insertText(editor, "B");
        command.execute();
        undo.push(command);
        undo.pop().undo();
        redo.push(command);
        insertText(editor, "?").execute();
        redo.length = 0;
        console.log(editor.text + " redo=" + redo.length);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeTrace a middleware onion

          Type the exact order of the middleware log.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const log = [];
          const outer = (next) => { log.push("outer before"); next(); log.push("outer after"); };
          const inner = (next) => { log.push("inner before"); next(); log.push("inner after"); };
          outer(() => inner(() => log.push("handler")));
          console.log(log.join(" > "));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            Structural and behavioral patterns quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which sentence best defines a decorator pattern in JavaScript?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the express shipping strategy print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const shippingStrategies = {
                standard(cart) { return cart.subtotal >= 75 ? 0 : 7; },
                express(cart) { return 12 + cart.items * 2; },
                pickup() { return 0; },
              };
              const cart = { subtotal: 80, items: 2 };
              console.log(shippingStrategies.express(cart));

              Choose an answer to see the explanation.

            3. Question 3 of 7Which pair is most accurate?

              Choose an answer to see the explanation.

            4. Question 4 of 7What is the final command-manager state?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const editor = { text: "Hi" };
              const undo = [];
              const redo = [];
              function insert(value) {
                return {
                  execute() { editor.text += value; },
                  undo() { editor.text = editor.text.slice(0, -value.length); },
                };
              }
              let command = insert("!");
              command.execute();
              undo.push(command);
              undo.pop().undo();
              redo.push(command);
              command = insert("?");
              command.execute();
              undo.push(command);
              redo.length = 0;
              console.log(editor.text + " redo=" + redo.length);

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the middleware onion print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const log = [];
              const outer = (next) => { log.push("outer before"); next(); log.push("outer after"); };
              const inner = (next) => { log.push("inner before"); next(); log.push("inner after"); };
              outer(() => inner(() => log.push("handler")));
              console.log(log.join(" > "));

              Choose an answer to see the explanation.

            6. Question 6 of 7When is a proxy a better fit than a decorator?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which warning keeps these patterns production-friendly?

              Choose an answer to see the explanation.

            Key takeaways

            • Decorator wraps behavior; proxy controls access to a target.
            • Adapter translates an interface; facade simplifies a subsystem.
            • Strategy chooses an algorithm at runtime without editing the caller.
            • Command turns actions into objects so history can execute, undo, and redo them.
            • Middleware chains functions through next(); async middleware uses await next() to create onion order.
            One-line summary

            Use patterns to name real seams: wrapping, access control, API boundaries, runtime choices, undoable actions, and pipelines.

            Next, state management uses actions, reducers, and single-source-of-truth ideas to keep application state predictable as features grow.

            CompleteFrontend Clear concepts. Working examples.