Strategy Pattern in Node.js
Part of my Node.js Design Patterns series.
Every delivery platform calculates its commission differently. One takes a flat percentage. One takes a percentage with a minimum. One has a tiered rate that changes by monthly volume, and a promotional rate on Wednesdays. The first implementation of "compute the net payout" was a single function with a switch on partner name and a nest of if inside each case. Reading it was fine. Changing it was not, because every partner's rules sat in one scope, and a fix for one had a way of touching the others.
The Strategy pattern is the answer to "same step, different rules": one interface, several interchangeable implementations, and the caller picks one at runtime. In JavaScript the interface is just a function signature, so the pattern collapses to a map of functions.
The pattern in modern Node
// Every strategy has the same shape: (order, context) => fee in the order's currency.
const strategies = {
flat: (order) => order.total * 0.2,
withMinimum: (order) => Math.max(order.total * 0.15, 15_000),
tiered: (order, { monthlyVolume = 0 } = {}) => {
const rate = monthlyVolume > 50_000_000 ? 0.12 : 0.18;
return order.total * rate;
},
};
export function commissionFor(partner) {
const strategy = strategies[partner];
if (!strategy) throw new Error(`No commission rule for "${partner}"`);
return strategy;
}
export function netPayout(order, context) {
const fee = commissionFor(order.partner)(order, context);
return Math.round(order.total - fee);
}import { netPayout } from "./commission.mjs";
const orders = [
{ id: "A1", partner: "flat", total: 120_000 },
{ id: "A2", partner: "withMinimum", total: 60_000 },
{ id: "A3", partner: "tiered", total: 200_000 },
];
for (const order of orders) {
console.log(order.id, netPayout(order, { monthlyVolume: 80_000_000 }));
}Each rule is a few lines that can be read, tested, and changed alone. The Wednesday promotion goes inside tiered and touches nothing else.
Testing strategies in isolation
Because every strategy shares a signature, one table-driven test covers all of them, and adding a partner means adding a row:
import { test } from "node:test";
import assert from "node:assert/strict";
import { commissionFor } from "./commission.mjs";
const cases = [
["flat", { total: 100_000 }, {}, 20_000],
["withMinimum", { total: 50_000 }, {}, 15_000],
["tiered", { total: 100_000 }, { monthlyVolume: 0 }, 18_000],
["tiered", { total: 100_000 }, { monthlyVolume: 60_000_000 }, 12_000],
];
for (const [partner, order, context, expected] of cases) {
test(`${partner} on ${order.total} with ${JSON.stringify(context)}`, () => {
assert.equal(commissionFor(partner)(order, context), expected);
});
}This is the argument for the pattern in one file. The switch version needed a test per branch that also proved the other branches were not accidentally hit.
Strategy versus config flag
A question I get in code review: why not a config object with rate and minimum fields, and one function that reads them? Do that when the shape of the rule is the same and only the numbers differ. Use strategies when the shape differs, when one partner needs volume history and another needs the weekday. The moment a config object grows fields that only some partners use, and if (config.minimum !== undefined) shows up, you have strategies wearing a config costume.
Where it goes wrong
Strategies that reach outside. A strategy that queries the database for monthly volume is now impossible to test without one. Pass what it needs in context, resolved by the caller. The strategy computes; the caller gathers.
Selection logic sprinkled everywhere. If four call sites each do strategies[order.partner], the day a partner is renamed you fix four places. Keep one commissionFor() and make everyone go through it.
Classes for the sake of it. A CommissionStrategy base class with calculate() is fine in Java. In JavaScript it adds a file per rule and hides that the rule is one expression. Reach for a class only when a strategy has real state to carry between calls.
When not to use it
Two implementations that will never become three do not need a registry; a plain if is honest and shorter. And if the variation is per-customer rather than per-kind (every shop negotiates its own rate), that is data, not strategy: put the rate in the shop record.
Next: the Middleware Pattern, for steps that must run in a fixed order and each get a chance to stop the chain.

Nguyễn Hải Nam
Nguyễn Hải Nam
Project Management Lead. 16+ years from code to delivery. PMP®. Writing here about project management and engineering.