Skip to main content

Compensation Architecture — Validation / Red-Team Pass (v2)

Status: Proposal for review — no production code. Stress-tests COMPENSATION_ARCHITECTURE_REVIEW.md and revises the recommendation. Date: 2026-06-22. Bottom line up front: After actively trying to disprove it, I no longer recommend Option C as written. Option C was partly right (the earnings duplication is real; payroll must be a pure, fully-snapshotting engine) and partly wrong (it over-merged — collapsing earnings and deductions, ignoring Benefits, and attaching compensation to the person instead of an employment/contract). The revised recommendation is Option C2 (§9).


1. Challenge Option C (argue against my own proposal)

Pretend a second senior architect with enterprise-payroll scars reviews Option C. Where does it break?

1.1 "Earnings and deductions are the same thing with a sign" — the weakest claim

  • Workday does not do this. Compensation (what you earn) and Payroll Deductions are different functional areas with different owners. Deductions largely originate in Benefits (elections → contributions) or as payroll deductions (garnishments, loans). Merging them into one CompensationItem imports a model SAP/Workday deliberately separate.
  • SAP is more nuanced: infotype 0014 is "Recurring Payments/Deductions" — so flat recurring +/- share a table. But SAP keeps 0008 Basic Pay (grade/structured base) separate from 0014, 0015 Additional Payments (one-time) separate again, and employer contributions are computed by the schema, never stored as employee wage types. So even SAP's "unification" is only of flat recurring items — not of base, not of one-time, not of contributions.
  • A union deduction, a court garnishment, a SACCO contribution, a loan repayment, and a housing allowance are not variants of one concept. They differ in owner, authorization, lifecycle, remittance, and tax treatment. Treating them as category: EARNING|DEDUCTION on one row hides those differences and will leak complexity later (e.g. garnishment caps, third-party remittance, benefit enrollment) into a model that started "elegant."

1.2 Missing Benefits bounded context

  • Option C has no Benefits concept. Yet pension is exactly a benefit: 7% employee contribution (a deduction) + 11% employer contribution (a cost, remitted, not in net). Today the pension rule computes both and the entry snapshots employer-pension (pension-rule.ts:26,51; payroll-entry.ts:32; schema.prisma:1733,2783). Insurance, provident funds, medical, gratuity all follow this employee-contribution + employer-contribution shape. Cramming these into CompensationItem(DEDUCTION) loses the employer side and the plan/enrollment lifecycle. Enterprise HR, banking, public sector, universities, hospitals all run rich benefits — this is not optional at scale.

1.3 Compensation attached to the person, not an employment/contract

  • Option C puts items on Employee. Real-world breakers:
    • Hospitals/universities: a consultant with a hospital appointment and a university lectureship — two contracts, two pay packages, possibly two legal entities.
    • Multiple legal entities: group employers split an employee across entities; each needs its own comp + payslip + filing.
    • Manufacturing: an employee moves between cost centers/shifts.
  • SAP (PA assignments), Workday (multiple Positions/Jobs per Worker), Oracle (assignments) all attach compensation to an assignment/contract, not the bare person. Option C cannot represent two concurrent packages without redesign.

1.4 "SalaryStructure → optional copy-on-apply template" — too weak for the sectors that need structure

  • Collective bargaining / public sector / universities / government: pay is grade × step with negotiated scales. A CBA raise means "all Grade 7 Step 3 get the new rate." A dumb copy template can't propagate that — you'd re-stamp thousands of employees. These sectors need dynamic grade inheritance with controlled, effective-dated propagation, which Option C explicitly discarded.

1.5 Manufacturing / hourly / shift — earnings aren't all "fixed recurring items"

  • Shift premiums, piece rates, hourly wages are derived from time & attendance, not stored as fixed CompensationItems. Option C's recurring-item model assumes monthly fixed amounts; it has no first-class link to Time & Attendance. (Overtime already hints at this — it's hours-derived and kept separate, correctly.)

1.6 Banking / executive comp — bonus & deferred plans are their own world

  • Incentive plans, deferred comp, clawbacks, stock/awards have eligibility rules, vesting, and accruals. Workday models these as distinct compensation plan types, not flat items. Option C's single item type would force plan logic into ad-hoc fields.

Verdict on §1: Option C's merge instinct optimizes for fewest tables, which is the wrong objective. The places it breaks (benefits, contracts, grades, time-derived pay, plans) are exactly where DemozPay is heading as it moves up-market.


2. Are these actually different business concepts? (no merging by resemblance)

For each entity: purpose · lifecycle · owner · frequency · bounded context · verdict.

EntityPurposeLifecycleOwnerFrequencyContextSame as others?
Salary Component (base)The contracted base/grade paySet at hire; changes on promotion/raise (effective-dated)CompensationRecurringCompensationDistinct from allowances (base is the anchor; grade-driven)
AllowanceEmployer-granted recurring earning (housing, transport…)Granted; revised; ended; effective-datedCompensationRecurringCompensationSame concept as an "earning component"; today's EmployeeAllowance ≈ a SalaryComponent(TAXABLE/NON_TAXABLE_ALLOWANCE). Real duplication — unify (earnings side only).
ReimbursementRepay an expense the employee incurredClaim → approve → pay; usually non-taxable, non-recurringExpenses/Finance (feeds Payroll)One-timeFinance/Expenses (not Compensation)Different: cost reimbursement, not compensation; don't merge into allowances
Taxable BenefitNon-cash benefit with imputed taxable value (car, housing-in-kind)Enroll/assign; valued each periodBenefitsRecurringBenefitsDifferent: affects taxable base without cash to net; not an allowance
BenefitA plan the employee is enrolled in (insurance, provident, pension)Plan defined → employee enrolled → generates EE + ER amountsBenefitsRecurringBenefitsDifferent: produces both an employee contribution and an employer contribution
Employee ContributionThe employee's share of a benefit/statutory (pension 7%)Derived from a plan/rule each periodBenefits / Payroll-rulesRecurringBenefits / PayrollA deduction, but owned by the benefit/rule, not a granted comp item
Employer ContributionThe employer's share (pension 11%, insurance)Derived from plan/rule each periodBenefits / Payroll-rulesRecurringBenefits / PayrollNot in net at all — a cost + remittance. Cannot live in a "deduction from pay" model
Deduction (voluntary recurring)Union dues, SACCO, savings, cooperativeInstruct → active → end; effective-datedThird-party deductions (Payroll-owned instruction)RecurringPayroll (deduction instruction)Different from earnings; similar in shape to a mandate → generalize the mandate concept to cover voluntary recurring, not court-only
Payroll MandateCourt order / garnishmentAwarded (with lifetime cap) → active → suspend/resume → completePayroll/LegalRecurring until capPayrollDistinct: legal precedence, totalAwarded/totalApplied, beneficiary. Keep.
One-Time EarningBonus, retro pay, correction, top-upDraft → approve → posted to one run → voidPayroll (or Compensation for bonus plans)One-off (period-scoped)PayrollDistinct from recurring comp. Keep.
OvertimePay derived from hours × policyRecord hours → approve → posted; base snapshotPayroll + Time & AttendancePer period, variablePayroll / T&ADistinct: time-derived, not a fixed item. Keep.
Payroll AdjustmentPost-approval correction on a frozen runDraft → approve → posted → reversePayrollOne-off correctionPayrollDistinct: corrective, append-only, reversible. Keep.
Compensation Item (proposed)Per-employee effective earning lineAuthor → active → retireCompensationRecurringCompensationValid only for the earnings side (base + allowances + recurring earnings). Should not absorb deductions/benefits.

Conclusion of §2: The only genuine duplication is EmployeeAllowance ⇄ earnings-typed SalaryComponent/EmployeeCompensation (both are recurring earnings). Everything else is a distinct concept that merely contains money. The original Option C wrongly merged deductions, benefits, reimbursements, and contributions into one item. Merge earnings; keep deductions/benefits/contributions/one-offs/mandates/adjustments separate, each in its rightful context.


3. Ethiopian payroll validation

ItemWhere it lives in the revised modelNatural fit?
Transport / Housing / Position / Hardship / Telephone / Fuel / Acting allowanceCompensation earning components, each with taxable/pensionable + exemption caps (taxExemptCapSantim)✅ — caps are first-class (today only Workforce Allowance has them; must be preserved)
Taxable vs non-taxable allowancePer-component taxable flag + cap (e.g. transport exempt up to a threshold, excess taxable)✅ — needs partial taxability (cap → split taxable/exempt), which a single boolean can't express. Requires cap-aware split logic (gap in both current models)
Income tax (PAYE)Payroll rule (versioned brackets), snapshotted on run✅ already
Pension 7% EE / 11% ERBenefits/Payroll rule — EE contribution (deduction) + ER contribution (cost)✅ already computed by PayrollPensionRule; generalize to a Benefits pattern
Court ordersMandate (cap, beneficiary, precedence)✅ keep
Loan deductionsLending context via outbox; appears as a deduction line✅ keep (cross-domain)
Cooperative / SACCO / Union deductionsVoluntary recurring deduction instruction (generalized mandate) with third-party beneficiary/remittance⚠️ today's EmployeeDeduction has no remittance/beneficiary; needs a third-party-payee concept
Absence deductionsComputed by engine from unpaid days (already)✅ keep
Advances / salary advanceLending/EWA-style repayment OR mandate✅ keep (cross-domain)
Recurring deductions (generic)Voluntary recurring deduction instruction

Ethiopian findings: The revised model supports all of these only if we add (a) cap-aware partial taxability for allowances (transport/per-diem exemptions are capped, not all-or-nothing), and (b) a third-party payee/remittance attribute on deductions (union/SACCO/court remit to an external party). Neither current model handles these cleanly. Option C as written would have hidden them inside a generic item — worse than naming them.


4. International / future validation

RequirementRevised model supportNotes
Monthly / weekly / hourly payrollPay-frequency on the contract; engine period-drivenNeeds contract-level frequency (today on Employee)
Shift premiumsTime & Attendance → earnings (like overtime)Needs a T&A integration boundary; not fixed items
CommissionsCompensation plan (variable) or one-time earningPlan type if recurring/target-based
Stock / equity compSeparate comp plan (vesting, accrual)Do not model as a salary line
Bonus plansComp plan (eligibility, targets) → posts one-time earningsWorkday-style plan
Salary sacrificePre-tax deduction linked to a benefitNeeds taxStage=PRE_TAX + benefit link
Retroactive / back payOne-time earning + retro recompute against frozen snapshotsSnapshot design (§5) makes this safe
GarnishmentsMandate (cap, precedence)Keep
Multiple currenciesMoney is currency-aware; rules per jurisdictionCurrency on contract/legal entity
Multiple legal entitiesContract/assignment per legal entityOption C can't; revised model adds contract layer
Multiple contracts per employeeContract/assignment layerSame

International finding: the make-or-break is the contract/employment-assignment layer and treating bonus/stock/benefits as distinct plan types, not salary lines. Option C (flat items on the person) fails multi-contract and multi-legal-entity without redesign — a hard requirement for hospitals, universities, banking groups, and any cross-border expansion.


5. Snapshot design — the most important decision (expanded)

Agreed: this matters more than entity merging. If history is perfectly reproducible, we can evolve the live models freely. Today PayrollRunEntry stores only totals + a deductions JSON + frozen taxable/pensionable bases + rule-id pointers — the earnings breakdown, rates, contributions, and inputs are not snapshotted.

How enterprise systems do it

  • SAP "Payroll Results" (cluster RT/Results Tables): every period, per employee, SAP freezes the complete result — every wage type with amount, rate, and number (quantity), plus the inputs and the schema/rule versions used. Re-running is an explicit retro that creates a new result; the old result is never mutated. This is the gold standard: a payslip is reproducible decades later from its own stored result.
  • Workday: "Payroll Results" + "Pay Calculation Results" retain per-worker, per-run results with full result lines and audit; off-cycle/retro create new results.
  • Principle: a payroll run is an immutable financial document, not a view over current config.

Make PayrollEntry a self-contained, reproducible result — store everything needed to reprint and re-verify without touching any live table:

  1. Earnings lines (NEW): for each component — componentTypeId, code, name, amountKind, rate/quantity used, resolved amountSantim, taxable, pensionable, exemption-cap applied, taxable-vs-exempt split.
  2. Deduction lines (expand): type, source (statutory/benefit/mandate/loan/voluntary), amountSantim, basis, pre/post-tax stage, beneficiary, upstream ref.
  3. Tax: bracket schedule version id, taxable base, computed tax, per-bracket breakdown.
  4. Contributions: employee and employer (pension now; benefits later) with rate + base.
  5. Inputs: working days, unpaid days, hours by category, base snapshot, absence divisor, protection-floor min-net, currency.
  6. Rule/version pointers: tax rule, pension rule, protection policy, overtime policy, compensation version (the missing pointer today).
  7. Formula results: any FORMULA/PERCENT_OF_BASE resolved values with their inputs.

Storage: a versioned result document per entry (structured JSON column(s) on PayrollEntry, or child PayrollEntryLine tables for queryability) with a snapshotSchemaVersion. Never recompute a closed run; retro/correction creates new rows (already the PayrollAdjustment pattern). This is forward-only and additive — low-risk to introduce regardless of which model wins.

This is the single highest-value, lowest-risk change and should arguably ship first, independent of the merge debate.


6. Templates vs dynamic inheritance

ApproachHowProsCons
Dumb template (copy-on-apply) — Option C as writtenApply structure → copy values to employee itemsSimple; explicit; trivially snapshot-friendly; no runtime indirectionNo propagation — a grade-wide CBA raise must re-stamp every employee; drift between template and reality
Dynamic inheritance (Workday grades/profiles, SAP pay-scale + indirect valuation)Employee references grade/step; values resolved from the scale at runDRY; central, effective-dated propagation; ideal for CBA/public sector/gradesHarder to reason about "what did this person actually earn"; must resolve-and-freeze for history; eligibility rules add complexity
Hybrid (recommended): resolve-at-assignment + effective-dated grade with controlled propagationGrade defines defaults; assigning a grade resolves concrete components; a grade change is an effective-dated event that re-resolves affected employees on/after its dateCentral updates and concrete per-employee truth; snapshot-safe (run freezes resolved values)More design than dumb copy

Finding: Pure copy templates are too weak for the grade/CBA-driven sectors (public, universities, hospitals, banking). Pure dynamic inheritance is hard to snapshot. The right answer is resolve-and-freeze: grades/scales provide effective-dated defaults; the employee's effective components are concrete and authoritative; the run snapshot freezes them. So "structure as template" should become "grade/scale with effective-dated propagation," not a one-shot copy.


7. Bounded contexts — every entity exactly one owner

Ownership rules:

  • HR/Workforce owns the person + contract/assignment (NEW) + org catalogs. Does not own money amounts.
  • Compensation owns recurring earnings + grades/scales. Earnings only.
  • Benefits (NEW) owns plans + enrollments → employee and employer contributions.
  • Time & Attendance owns hours/shifts → feeds overtime/premiums.
  • Leave owns absences → feeds proration.
  • Performance owns bonus/incentive eligibility → posts one-time earnings.
  • Payroll owns statutory rules, deduction instructions (mandates + voluntary + third-party payee), one-offs/adjustments/carry-forward, and the immutable run snapshot. Calculates money; owns no HR master data.
  • Finance/Accounting owns the ledger, remittances, reimbursements/expenses.
  • Lending/EWA own their repayments (cross-domain via outbox, ADR-011).

This is more contexts than Option C, not fewer — but each entity has exactly one owner, and payroll calculates rather than owns master data (your stated ideal).


8. Migration risk (realistic estimates)

Estimates assume the revised target (C2). Relative effort, not calendar promises.

DimensionComplexityNotes
DB migrationHighNew: Contract/Assignment, EarningComponentType, CompensationItem, Benefits (Plan/Enrollment), generalized deduction instruction + third-party payee, PayrollEntryLine/snapshot columns. Migrate EmployeeAllowance→earnings items; split EmployeeDeduction by owner (voluntary→instruction, loan/EWA→stay cross-domain); materialize seed EmployeeCompensation→items; backfill a default single-contract per employee. Idempotent, santim-exact, per-tenant reconciliation.
API migrationHighNew Compensation + Benefits + Contract controllers; generalize deduction endpoints; deprecate EmployeeAllowance/EmployeeDeduction endpoints; engine read-path swap.
Frontend migrationMedium-HighOnboarding wizard + employee edit + settings repointed; new contract selector; benefits enrollment UI (later phase).
TestingHighGolden-master: run payroll for a fixture tenant before and after and assert identical net/tax/pension per employee; snapshot reproducibility tests; RLS-as-NOSUPERUSER; multi-contract + benefits unit/integration.
RolloutPhased + flaggedDual-read soak: engine can read new model behind a flag while old endpoints still write; reconcile; cut over per tenant.
RollbackPer-phaseSnapshot columns are additive (no rollback risk). Until decommission (final phase), legacy tables remain for instant revert. Point of no return only at the explicit decommission phase, gated on a clean reconciliation report.

Sequencing insight: the snapshot work (§5) and the contract layer are independent of the merge and de-risk everything else — do them first.


9. Final recommendation (honest, post-challenge)

My recommendation changed. I do not still recommend Option C as written. Option C correctly identified (a) the real earnings duplication and (b) that payroll must be a pure, fully-snapshotting engine — but it over-merged: collapsing deductions/benefits/contributions/reimbursements into one signed item, ignoring Benefits, using dumb-copy templates, and binding compensation to the person instead of a contract.

  1. Unify only the earnings side. One CompensationItem (base + allowances + recurring earnings), per contract, effective-dated, with cap-aware partial taxability. Replaces EmployeeAllowance and the earnings role of SalaryComponent/SalaryStructure/EmployeeCompensation.
  2. Do not merge deductions into compensation. Decompose by owner: statutory (Payroll rules), benefits (Benefits context, EE + ER contributions), legal (Mandate), voluntary recurring with third-party payee (generalized deduction instruction), loans/EWA (cross-domain). Replaces EmployeeDeduction by routing each kind to its rightful owner.
  3. Introduce a Benefits context for plans → employee + employer contributions (generalizing today's pension rule).
  4. Introduce a contract/employment-assignment layer so compensation attaches to a contract (multi-contract, multi-legal-entity, per-frequency).
  5. Grades/pay-scales with resolve-and-freeze, not dumb-copy templates (CBA/public-sector/grade support).
  6. Snapshot-first: make PayrollEntry a complete, reproducible result document (§5) — ship this first, independent of everything else.
  7. Keep one-time earnings, overtime, mandates, carry-forwards, adjustments — distinct concepts, distinct owners.

Why C2 is superior after trying to disprove it

  • Single source of truth — per concept. Earnings have one home (Compensation); each deduction kind has one owner; contributions live in Benefits. "One SoT" applied per business concept, which is stronger than one mega-table.
  • No duplication, no sync, no bidirectional mapping — same win as C, achieved by routing concepts to owners rather than collapsing them.
  • Clear bounded contexts; payroll owns no HR master data — your stated ideal, fully met.
  • Strongest auditability — the snapshot design (the part you flagged as most important) is elevated to first-class and ships first.
  • 10-year fit — survives benefits, grades/CBA, multi-contract, multi-legal-entity, time-derived pay, bonus/equity plans, and multi-country, precisely the cases that broke Option C.
  • Still simple for startups — an SME uses one contract, a handful of earning items, a couple of deduction instructions, and the statutory rules; Benefits/grades/multi-contract are present but optional.

What I explicitly retract from the original proposal

  • ❌ "Earnings and deductions are one signed CompensationItem." — wrong; separate them.
  • ❌ "Benefits can be a deduction item." — wrong; Benefits is its own context with employer contributions.
  • ❌ "SalaryStructure → dumb copy template." — too weak; use resolve-and-freeze grades.
  • ❌ "Compensation on the employee." — should be on a contract/assignment.
  • ✅ Retained: unify the earnings duplication; payroll as a pure, fully-snapshotting engine; keep one-off/overtime/mandate/carry-forward/adjustment distinct.

Next step

If you broadly agree with C2, I'll revise ADR-028 to record C2 (it's still Proposed), produce the detailed C2 ER + phase plan, and we sequence snapshot-first. If any part of this validation is itself wrong (e.g. you don't foresee multi-contract, or Benefits is out of scope for years), tell me and I'll right-size the recommendation — the architecture should match DemozPay's real 10-year trajectory, not a generic enterprise checklist.