·
7 min read
·
Written by Tomáš Mikeš
MLM commission calculations: how to model them so they survive an audit
For JUST we replaced a legacy system with complex commissions. Recalculation history, versioned rules, reconstructing a computation years later — here's how to approach it so it survives the first auditor.
Multi-level marketing commissions are a discipline of their own. Seller A recruits seller B, B does 50 000 CZK/month in sales. A earns commission on B's volume — how much depends on A's tier, which qualification A held that month, whether A hit the bonus threshold, and whether A's override bonus on sellers C and D (both under B) was unlocked.
For JUST we replaced a system that had been doing these calculations for 15 years. Rule #1 when replicating: the new system must return the same number if you run both side by side on the same data. Rule #2: three years from now you must be able to explain why a seller got 3 240 CZK instead of 3 470 CZK.
Here's how we structured it.
1. Commission rules as data, not code
A developer's instinct: “I'll write calculateCommission(seller, month) with if/else on tiers.” Works for 6 months. Then business lands a new bonus and you amend the function. Then a change, then another. Three years in you have 6 versions of the function and you can't separate them by the month they applied.
The right way: rules as rows in a DB table with validFrom / validTo. Schema:
CommissionRule {
id: UUID
name: 'Tier 2 Base Commission'
validFrom: Date
validTo: Date | null
condition: JSON // when it applies
calculation: JSON // what is computed
priority: Integer
}Computing for month 2023-05 loads rules where validFrom <= '2023-05-01' AND (validTo IS NULL OR validTo > '2023-05-31'). A business logic change = a new row, not a code edit. Historical periods are recomputed with the rules that applied at the time.
2. Calculation outcomes are immutable
Once a commission is calculated and the seller is paid, it is not rewritten. Even if a bug in the rules is found. Instead of overwrites, a correction is recorded.
Schema:
CommissionOutcome {
id: UUID
sellerId
month: '2023-05'
amount: 3240
ruleSnapshot: JSON // every rule used, as copy
inputSnapshot: JSON // sales volumes, network at calc time
calculatedAt: DateTime
status: 'final' | 'corrected' | 'superseded'
}
CommissionCorrection {
id: UUID
originalOutcomeId
correctionAmount: 230 // +/-
reason: string
approvedBy: UserId
approvedAt: DateTime
}Payout to the seller = original + all corrections. The financial system has an immutable audit trail. In 2026 when an auditor asks “why was this 2023-05 commission 3 240 CZK?” you can replicate it — the snapshot of rules and inputs is there forever.
3. Test scenarios as a business-engineering contract
Business says: “seller Jana has volume 80 000, under her is B with 60 000, under B is C with 40 000, Jana gets 3% override on B and 1.5% on C. Compute.” We run it, get a number, business confirms “correct.” That must be stored as a test case.
For JUST we collected a scenario file — ~50 real and hypothetical cases. Every rule in the system has at least 2-3 scenarios that cover it. When someone changes the logic, CI runs all scenarios and shows which results changed. If they changed unexpectedly → bug.
The legacy system had nothing like it. After 15 years nobody knew exactly how something was computed — it had to be reverse-engineered. One of the first deliverables of our implementation was getting scenario expectations from a business analyst and thereby pinning down the actual rules.
4. Dual-run before the cutover
Before legacy → new cutover we ran both systems in parallel for 3 months on the same data. Results were compared every month. A delta > 0.01% triggered an alert.
We hit 7 rules where the new system computed differently. Five times the legacy was right and we had a bug. Twice the legacy was wrong and the business had never noticed (~15 000 CZK underpaid across 3 months). Reported, sellers got corrections, business appreciated it.
Dual-run isn't a luxury. For critical financial systems it's the minimum to launch without surprises.
5. Recalculation capability from day one
Being able to recompute any historical month is load-bearing. Reasons:
- Audit finds a bug in 2022 rules — you must recompute the entire history
- Legal dispute with a seller — you must present the detail of the calculation
- Business wants to retroactively apply a new bonus — recalculation with a retroactive rule
If you have immutable rules in the DB + input snapshots per calculation, recalculation is deterministic. If you have logic in code and you change it routinely, three years on recalculation is impossible.
Generalising
MLM isn't the only use case. The same pattern applies to:
- Loyalty programs with changing rules
- Payroll / comp systems
- Pricing engines where customers have historically negotiated terms
- Fintech fee calculations with regulatory changes
Common denominator: today's rule ≠ next year's rule, and you have to defend both. Don't put it in code. Put it in data, with immutable history and tests as a contract between business and engineering.
Working on something similar?
Book a 30-minute technical call. No sales process — direct architectural feedback.
Our service:
Build systems that scale — without bottlenecks →