I built the task routing system before we knew the rules
Plantronic's routing policy was unfinished. A small rule language and manager UI let client and developer work in parallel—and produced a system that lasted.
- Published
- Reading time
- 12 min read
The routing rules did not exist yet. I started building anyway.
During specification, the application manager and I knew the outcome Plantronic needed: tasks arriving from project management had to reach the right production team. What we did not have was a finished matrix of every rule. Team responsibilities were still being clarified. The relevant task attributes were becoming visible, but their final combinations were not settled.
The obvious response would have been to wait.
The faster-looking response would have been to hardcode our current assumptions, ship them, and promise to revisit the code once the organization had made every decision.
I suggested a third option: build routing as a small system whose behavior came from data, then give the application manager an interface to manage that data himself.
This was a wager. A data-driven feature needs a model, validation, an evaluator, failure behavior, persistence, and a usable management screen. A hardcoded conditional needs only a few lines. But the apparent shortcut would have coupled my implementation schedule to an unfinished organizational specification. The system let both of us continue working.
It also became one of those features whose value is easier to see in hindsight. The implementation survived while the routing data could evolve.
We separated the mechanism from the policy
The stable part of the requirement was already clear.
A task arrived with known attributes. Some combination of those attributes determined which team should receive it. Plantronic had to apply that decision consistently, save the assigned team, and reject tasks for which no route existed.
The unstable part was the policy: the actual combinations of attributes and teams.
Hardcoding would have placed both parts in one function. Each organizational change would then become a software change:
cond do
task_has_this_combination? -> first_team
task_has_that_combination? -> second_team
true -> raise "unroutable"
end
The exact syntax is not important. The coupling is. A manager changing responsibility would need a developer. The developer would need a new specification, code change, test adjustment, review, and deployment. Until the policy was complete, implementation would either stop or repeatedly chase a moving target.
Instead, I modeled a task route as data. Each route belonged to a destination team and held a bounded set of labels to match against an incoming task. A route matched only when all its configured labels matched. The first matching route selected the team. If none matched, task creation failed explicitly rather than leaving unassigned work in the system.
This created a clean division:
- I could build and test how routing works;
- the application manager could decide which routes should exist;
- Plantronic could enforce which task attributes are valid routing inputs.
The manager interface completed that division. It listed teams and their routes, allowed multiple routes for one team, and let an authorized manager add or remove both routes and match criteria. The behavior was configurable, but not arbitrary. Users could compose rules from concepts the application understood; they could not upload code or invent an unvalidated language.
That boundary is what made the design useful rather than merely “flexible.”
Data-driven does not mean unconstrained
Configuration systems often go wrong by trying to make everything configurable. They gradually become programming languages with worse tooling: strings for types, hidden precedence, no refactoring support, and production as the test environment.
I wanted the opposite.
Plantronic’s routing language was deliberately small. A rule contained a destination team and selected match labels. The application owned the allowed dimensions. Ecto changesets guarded the records. The evaluator had one deterministic meaning. Task creation and team assignment happened in one database transaction.
That last part mattered. An unknown combination did not create a half-valid task and ask somebody to repair it later. Routing either found a team and inserted the task, or returned an unroutable error and inserted nothing.
Persisting the selected team also separated new policy from past decisions. Changing route data affected future imports. It did not silently redistribute tasks that planners had already received.
There were still trade-offs. The first matching route won in creation order, so ordering was part of policy even though it was not represented by an explicit priority field. With a small, managed rule set this was workable. In a larger rules system I would expose precedence, detect overlapping rules, and offer a “why did this match?” explanation before adding more expressive conditions.
A useful data-driven system is not one that can express anything. It is one that can express the changes its owner genuinely needs while making invalid states and ambiguous behavior difficult.
The unfinished specification became parallel work
The architectural benefit was real, but the collaboration benefit was larger.
The application manager was not withholding a finished spreadsheet from me. He was doing domain work: determining how teams should be organized and how incoming tasks should be distributed. That work involved people, responsibilities, and operating details outside the codebase. It could not be accelerated by asking for a final answer more insistently.
Meanwhile, the software problem was concrete enough to solve. I knew routes needed match criteria, teams, deterministic evaluation, transactional assignment, and an administration interface. I could implement those constraints without knowing every eventual row of routing data.
So we worked in parallel.
I built the mechanism. He refined the policy. Once the interface existed, he could enter and adjust the resulting routes directly instead of translating each organizational decision into another development request.
This is an underrated form of decoupling. Software architecture usually discusses coupling between modules or services. Projects also contain coupling between people’s work. If a developer cannot proceed until a domain expert finalizes volatile data, and the domain expert cannot test decisions until the developer hardcodes them, the delivery process contains a dependency cycle.
A well-bounded management interface can break that cycle.
It does not remove collaboration. We still had to agree on vocabulary, constraints, and failure behavior. It moves collaboration to the stable boundary and allows each person to continue independently on the variable details.
Git shows two short bursts across 37 elapsed days
I went back through Plantronic’s history to see how long this actually took.
Git cannot tell me hours worked, and the main feature commits were squashed. Claiming an exact effort total would be fiction. It does preserve enough evidence to reconstruct the delivery shape.
The first routing migration is timestamped May 18, 2025. The core feature was committed and merged on May 20. That first implementation introduced the route schema and context, assigned teams during external task creation, handled unroutable tasks transactionally, and added focused tests.
The manager-facing phase began no later than a migration timestamped June 22. On June 24 at 12:33, the repository recorded the routing management interface and supporting context operations. A formatting pass followed nineteen minutes later. A final focused cleanup landed at 19:18 that evening.
From the first recorded source artifact to the last focused cleanup, 37 days elapsed.
That does not mean 37 days of coding. The evidence points to two concentrated implementation bursts of roughly two calendar days each, separated by a month in which other Plantronic work continued and the routing policy could mature. Migration timestamps are not time sheets, so even “roughly four days” describes elapsed implementation windows, not billable effort.
This distinction supports the architectural story. The gap was not dead time spent waiting for one giant specification. The core routing mechanism already existed. Other development continued. The manager interface arrived when the policy needed to become directly manageable.
The first commit was broad because routing crossed real boundaries: schema, import transaction, team relation, UI, migrations, and tests. The administration phase added the operations and screens needed to hand policy ownership to the application manager. This was more work than a hardcoded branch, but still a small turnaround for a feature that removed future deployments from ordinary routing changes.
Three established patterns explain the design
I did not implement a general rules engine, and describing Plantronic that way would oversell it. The design does sit at the intersection of several established patterns.
Content-Based Router
Gregor Hohpe and Bobby Woolf’s Content-Based Router sends a message to a destination based on data in that message. Their pattern also warns that routing becomes a frequent maintenance point and notes that sophisticated versions may use configurable rules.
Plantronic routed tasks to teams rather than messages to channels, but the structural problem was the same: inspect content, select one destination, and keep the sender independent of destination details.
The pattern helped clarify what the routing mechanism should own. It should evaluate task content and return a destination. It should not require project management to know Plantronic’s current team structure.
Parameterized Specification
Eric Evans and Martin Fowler’s paper on Specifications makes an even closer distinction. A specification separates criteria from the candidate being tested. Their Hard Coded Specification requires programming for each new rule. A Parameterized Specification allows users to create new specifications at runtime, but only within capabilities programmers have provided.
That is almost exactly the trade Plantronic made.
The match labels formed a parameterized specification for an incoming task. The application manager could create new combinations without a deployment. I still defined the legal vocabulary and matching semantics. We gained runtime adaptability without pretending the domain expert should become a programmer.
Evans and Fowler also describe a composite form where users combine specifications into a richer language. Plantronic did not need that power. Exact conjunctions of a few known labels covered the requirement. Choosing the least expressive model that solves the problem kept evaluation and testing small.
Decision tables and DMN
At the formal end of this spectrum sits the Object Management Group’s Decision Model and Notation. DMN separates business decisions and rules from process models and aims to make them readable by business people, analysts, and developers. Decision tables can express multi-criteria policies in an unambiguous form.
Plantronic did not need a DMN engine, graphical notation, or a general decision table editor. Referencing DMN is still useful because it names the larger architectural move: organizational decisions deserve an explicit model instead of disappearing inside process code.
The scale should match the problem. For Plantronic, a relation, a small map of criteria, a deterministic matcher, and two administration screens were enough.
Elixir made the safer version cheap enough
Data-driven behavior moves risk. Hardcoded code can fail because the implementation is wrong. Configurable behavior can fail because the evaluator is wrong, the stored data is invalid, two rules overlap, or no rule matches.
The answer is not to trust configuration. It is to test the constraints and semantics as first-class code.
The initial routing implementation included a focused ExUnit test module plus expanded task and controller coverage. The tests established several important properties:
- a route requires both match criteria and a team;
- matching returns the expected team;
- precedence is deterministic when more than one route could match;
- an unknown combination returns an explicit error;
- an unroutable import leaves the task count unchanged;
- external task creation accepts only its intended fields.
ExUnit makes these examples compact and readable. Factories can create teams, tasks, and routes with only the attributes relevant to each case. Pattern matching asserts both result shape and domain value without much ceremony.
The Ecto SQL Sandbox gives database tests isolated transactional connections. That matters for routing because the valuable guarantee crosses the database boundary: an unroutable task must not persist. I can test the actual context and transaction instead of replacing the repository with mocks and hoping production behaves similarly.
Elixir did not make a data-driven design automatically safe. It made the feedback loop short enough that building the model, evaluator, constraints, and failure cases remained practical. The safer architecture did not require a multi-month rules-engine project.
When I would make this choice again
I would not turn every changing conditional into database rows and an admin screen. Hardcoded policy is often correct when rules are few, stable, developer-owned, and released with the rest of the application.
I would seriously consider data-driven behavior when several conditions hold:
- the mechanism is stable but policy is still evolving;
- a domain owner, not a developer, is responsible for future changes;
- waiting for complete policy would block implementation;
- the allowed inputs and operations can be tightly bounded;
- wrong or missing rules have explicit, testable failure behavior;
- changing policy without a deployment has operational value.
I would push back when “configurable” means arbitrary expressions, invisible precedence, or an attempt to avoid understanding the domain. At that point configuration becomes a second application, usually with worse tools than the first.
Plantronic stayed on the useful side of that line. Its current code still carries the same essential model: routes hold match labels and belong to teams; the evaluator selects a team from incoming task data; manager operations create, change, and remove the route data. Later work adjusted inputs and refined the interface, but did not require replacing the routing core.
That is what standing the test of time looks like in a business application. Not frozen code. A stable mechanism with an intentional place for change.
Build the place where the answer will live
When a specification is unfinished, waiting can feel disciplined and hardcoding can feel agile. Sometimes both reactions miss the better option.
We knew enough to define the shape of a valid answer before we knew every answer. By turning that shape into a constrained data model and giving its owner a proper interface, I could build in parallel with the person resolving the organization itself.
The extra architecture cost time up front. It also removed a queue of future code changes, let Plantronic adapt without deployments, and kept routing ownership close to the person who understood it best.
The most interesting part is still the apparent contradiction: I built the routing system before we knew the rules.
It worked because I did not build the rules. I built the tested place where the rules could live.