Nicolás Duque

Rules as data: onboarding customers without shipping code

Sep 16, 2026 · 2 min read

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.

What stays in code versus what moves to configuration when every customer has its own rules, and the discipline configuration then needs
The split that let us onboard a restaurant without a release.

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:

  1. Version them, so you can see who changed what and roll back.
  2. Validate them before they're saved: the accounts exist, the rule is complete, it doesn't conflict with another rule.
  3. Test them against real sample transactions before they go live for a customer.
  4. 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.

More in Architecture · All writing