25 March 2026
Design patterns I use in production
A config-driven approach to building scalable, maintainable webhook systems without conditional complexity.
When I recently built a webhook system for multiple stores, I wanted something scalable without filling the codebase with conditionals. Different stores had different needs — some required multiple webhooks, others just one. Instead of hardcoding logic, I moved everything into a configuration object and let the system execute based on data.
Core idea
The entire per-store webhook config lives in a single object:
webhooks: {
stores: {
[Store.StoreOne]: [
{
url: `url1`,
authHeaders: { ... },
responseType: 'storeone'
},
{
url: `url2`,
authHeaders: { ... },
responseType: 'management'
}
],
[Store.StoreTwo]: [
{
url: `url2`,
authHeaders: { ... },
responseType: 'management'
}
]
}
}
Using computed property names like [Store.StoreOne] keeps keys in sync with the enum and avoids fragile string literals. With Record<Store, ...>, TypeScript ensures only valid stores are used. This turns what could be conditional logic into a simple lookup:
return webhooks.stores[store];
Four properties that make this pattern hold up
Open for extension
Adding a new store is just adding config — no service-layer changes required:
[Store.NewStore]: [ ... ]
The system picks it up automatically. There's no conditional to update, no switch statement to extend, no risk of forgetting a branch.
Data-driven fan-out
The system doesn't care how many webhooks a store needs. It just loops over whatever the config says:
const results = await Promise.all(
configs.map((c) => {
return doSomething;
})
);
Clean and predictable. A store with one webhook and a store with five are handled identically.
Response handling stays simple
Instead of branching on store, responses are handled by type:
const response = await fetch();
if (responseType === "management") {
// additional check for response details
// throw if response details not matching with conditions
}
Rather than branching on store inside an HTTP handler, the discriminant is the narrower responseType. This means two different stores can share the same response format without duplicating parsing logic.
Auth is co-located
Each webhook entry defines its own authHeaders, so the HTTP layer stays generic and reusable. There's no auth-switching logic scattered across the codebase — the config is the single source of truth for how each endpoint authenticates.
Closing
Takeaway
Move behaviour into data.
It keeps your codebase easier to extend, easier to read, and much harder to break as new integrations are added. The pattern scales naturally — the more stores or webhook types you add, the more the config-driven approach pays for itself compared to a conditional-heavy alternative.