Rules as data: onboarding customers without shipping code
KitchenSync automates accounting for restaurants in the US. Every restaurant group does its books a little differently: its own chart of accounts, its own way of splitting sales, fees and payroll, its own exceptions. For a while, each of those differences lived in code.
The problem
Every new customer meant a pull request. Someone read the customer's requirements, wrote a special case, reviewed it and deployed it. Onboarding depended on engineering capacity, and the codebase slowly filled with if customer === ... branches that nobody wanted to touch.
The decision
We turned the rules into data: per-customer configuration evaluated by a shared engine. A rule describes what to match and what to post, and the engine applies it. Onboarding a customer became configuring rules, not writing code.
The alternative we rejected was a module per customer. It feels simpler at first because each module is easy to read, but the total cost grows with every customer, and fixing a bug means fixing it N times.
The trade-off
Rules as data doesn't remove complexity, it moves it:
- The engine gets harder. It has to handle every shape of rule, report clearly why a rule did or didn't match, and stay fast.
- You need tools for the people who configure. If only engineers can write the configuration, you've just invented a worse programming language.
- Configuration can break production too. A bad rule posts bad entries just like a bad deploy.
Treat configuration like code
That last point is the lesson I'd pass on. Once rules are data, give them the same care as code:
- Version them, so you can see who changed what and roll back.
- Validate them before they're saved: the accounts exist, the rule is complete, it doesn't conflict with another rule.
- Test them against real sample transactions before they go live for a customer.
- Trace them: every posting records which rule produced it, so "why is this here?" has an answer.
This design also paid off later, when we added LLMs. A model suggestion and a rule could share the same trail: each posting said whether a rule, a model or a person made the call. I wrote about that in LLMs in production: the hard part is the contract around the model.