Skip to content
JavaScript js patterns 4 min read

State & Other Patterns

Beyond the headline patterns, a handful of smaller behavioral and structural patterns quietly solve recurring problems in everyday JavaScript. The state, command, and adapter patterns each tame a specific kind of complexity: tangled conditionals, scattered action logic, and mismatched interfaces. This page gives you a fast, practical tour of all three with runnable examples and clear guidance on when to reach for each.

State pattern

The state pattern lets an object change its behavior when its internal state changes, so it appears to switch classes at runtime. It is the object-oriented cure for sprawling if/switch blocks that branch on a status string. Instead of asking “what state am I in?” everywhere, each state becomes its own object that knows how to handle events and which state comes next.

Think of a media player. The play action means different things depending on whether the player is stopped, playing, or paused. A finite state machine captures the legal transitions explicitly.

const transitions = {
  stopped: { play: "playing" },
  playing: { pause: "paused", stop: "stopped" },
  paused:  { play: "playing", stop: "stopped" },
};

class Player {
  constructor() {
    this.state = "stopped";
  }
  dispatch(action) {
    const next = transitions[this.state][action];
    if (!next) {
      console.log(`Ignored "${action}" while ${this.state}`);
      return;
    }
    console.log(`${this.state} --${action}--> ${next}`);
    this.state = next;
  }
}

const p = new Player();
p.dispatch("play");
p.dispatch("pause");
p.dispatch("stop");
p.dispatch("pause"); // illegal from stopped

Output:

stopped --play--> playing
playing --pause--> paused
paused --stop--> stopped
Ignored "pause" while stopped

The transition table is data, not branching logic, which makes illegal transitions impossible to trigger by accident. For richer behavior, swap the string states for objects that carry their own methods.

Tip: A declarative transition map like the one above is the easiest finite state machine to test — you can assert on the table directly without simulating events. Libraries such as XState formalize this idea with guards, side effects, and nested states.

Command pattern

The command pattern wraps a request as a standalone object, decoupling the code that triggers an action from the code that performs it. Because each command is a first-class value, you can queue them, log them, pass them around, and — crucially — implement undo by pairing every execute with an undo.

class History {
  constructor() {
    this.done = [];
  }
  run(command) {
    command.execute();
    this.done.push(command);
  }
  undo() {
    const command = this.done.pop();
    if (command) command.undo();
  }
}

const makeAddText = (doc, text) => ({
  execute: () => { doc.content += text; },
  undo:    () => { doc.content = doc.content.slice(0, -text.length); },
});

const doc = { content: "" };
const history = new History();

history.run(makeAddText(doc, "Hello"));
history.run(makeAddText(doc, ", world"));
console.log(doc.content);
history.undo();
console.log(doc.content);

Output:

Hello, world
Hello

Each command is a small object with a uniform interface, so the History invoker never needs to know what any individual command actually does. This is exactly how editors implement undo/redo stacks and how task queues serialize work.

Adapter and facade patterns

An adapter translates one interface into another so two incompatible pieces of code can collaborate. A facade is closely related: it hides a messy or sprawling subsystem behind a single, friendly entry point. Both are structural patterns about presenting a cleaner surface.

Suppose a third-party logger exposes writeLine, but your app expects a standard log method:

// Incompatible third-party API
const vendorLogger = {
  writeLine(level, message) {
    console.log(`[${level.toUpperCase()}] ${message}`);
  },
};

// Adapter: presents the interface our app expects
const loggerAdapter = {
  info: (msg) => vendorLogger.writeLine("info", msg),
  warn: (msg) => vendorLogger.writeLine("warn", msg),
  error: (msg) => vendorLogger.writeLine("error", msg),
};

loggerAdapter.info("Service started");
loggerAdapter.error("Disk almost full");

Output:

[INFO] Service started
[ERROR] Disk almost full

A facade goes a step further by orchestrating several calls behind one method. For example, a checkout(cart) facade might validate stock, charge payment, and send a receipt — callers never touch those subsystems directly. The adapter changes shape; the facade reduces surface area.

Choosing the right pattern

PatternTypeProblem it solvesReach for it when
StateBehavioralBehavior that depends on a changing internal modeStatus-driven if/switch chains keep growing
CommandBehavioralCoupling between trigger and actionYou need undo/redo, queuing, or logging of actions
AdapterStructuralTwo incompatible interfacesIntegrating a third-party or legacy API
FacadeStructuralA subsystem that is hard to use directlyYou want one simple entry point over many calls

Best practices

  • Reach for the state pattern only once branching on a status grows past two or three values — a single if does not need a state machine.
  • Keep state transitions declarative (a table or map) so invalid transitions are unrepresentable and easy to unit test.
  • Give every command a matching undo, and store executed commands in a stack to get redo nearly for free.
  • Keep commands free of UI concerns so the same command works from a button, a keyboard shortcut, or a script.
  • Use an adapter to quarantine third-party APIs behind your own interface — swapping vendors later then touches one file.
  • Prefer a facade over leaking subsystem details to callers, but do not let it become a god object that does everything.
  • Don’t apply a pattern preemptively; introduce it when duplication or coupling actually appears.
Last updated June 1, 2026
Was this helpful?