PATTERN Cited by 1 source
Pluggable component architecture¶
Pluggable component architecture takes microservices-style independently-deployable-components thinking and applies it inside a single in-process algorithm. A complex algorithm is decomposed into components with well-defined contracts between them, so each component can be rewritten, A/B-tested, or swapped wholesale without touching the others. Unlike microservices, there's no network boundary — the contracts are just type signatures — but the discipline of keeping behaviour on one side of a contract from leaking to the other is the same.
The problem this solves¶
Non-trivial algorithms tend to couple concerns that ought to be independent:
- Classic Dijkstra couples graph structure, edge costs, and traversal logic in one loop. Changing "how do we decide which edge to follow?" forces editing the same code that owns "how do we walk the graph?"
- A monolithic recommender couples feature extraction, ranking, and diversification.
- A monolithic router couples filtering, scoring, and path construction.
If coupled, any behavioural change risks regressing the other concerns, and A/B-testing one concern without the others is infeasible.
Canonical Canva split¶
Canva's routing engine breaks the algorithm into three components (see systems/canva-print-routing):
| Component | Responsibility | Contract |
|---|---|---|
| Build + Retrieve | Own the graph (build from source of truth, publish, retrieve) | "Give me the graph for this destination region" |
| Decision | Rank candidate paths | better(a, b), rank([paths]) |
| Traversal | Walk the graph to produce a route | Calls Build-and-Retrieve once, calls Decision per step |
As long as the contracts are preserved, any one of these can be rewritten in isolation. The worked example in the post is swapping in a randomized decision component — type swap, no other changes.
Benefits earned¶
- Safer refactors. Behavioural change in one component can't regress another.
- A/B-testable. Route some traffic through a new Decision engine without touching Traversal or Build-and-Retrieve.
- Independent scaling / evolution. Add a new data source to Build-and-Retrieve (e.g., new supplier metadata) and roll it out in sync with a new Decision rule that consumes it — no refactor across the whole engine.
- Component-level testability. Test the Decision engine against synthetic path lists, the Traverser against a fake Decision engine, etc.
Distinct from control-plane/data-plane separation¶
- Pluggable components is an in-process algorithm decomposition principle — all components run together, typically in the same request path.
- Control-plane/data-plane separation is a system-level principle — control plane decides, data plane delivers, running in separate processes / lifecycles. See concepts/control-plane-data-plane-separation.
Both are about contracts and independent evolution, but at different scales.
Costs / gotchas¶
- Contract discipline. The pattern breaks the moment one component reaches around its contract (e.g., Traversal reading a field on the graph that Build-and-Retrieve didn't promise).
- Interface design up front. You have to pay the cost of designing the contracts before you know every use case. Too narrow = future-hostile; too wide = the components aren't really independent.
- Overhead. There's real cost in abstraction — usually small in-process, but non-zero.
Seen in¶
- sources/2024-12-10-canva-routing-print-orders — three-way split (Build-and-Retrieve, Decision, Traversal) with contracts narrow enough that swapping the Decision engine is a type change.
Related¶
- concepts/control-plane-data-plane-separation — same spirit, different scale
- systems/canva-print-routing