Posting rules DSL¶
The execution layer for PAY-007, DT-012, and OPS-007. Converts policy intent into reviewable, version-controlled configuration rather than scattered code paths.
Design goals¶
- Readable by finance, product, risk, and engineering.
- Version-controlled and promotable through environments.
- Deterministic: the same rule package and the same input event always produce the same posting outcome.
- Explainable: every decision returns human-readable reason codes, machine-readable codes, and affected balance states.
- Safe to retry: rule execution is idempotent and never duplicates a financial effect.
Core objects¶
| Object | Meaning | Minimum fields |
|---|---|---|
event |
Business event received for processing | event_type, event_id, idempotency_key, effective_at, customer/account refs, amount, currency |
rule_package |
Versioned collection of posting rules active for a domain or product | package_id, version, effective_from, compatibility |
rule |
One executable mapping from event + conditions → outcomes | rule_code, priority, predicates, posting_template, reason_codes |
posting_template |
Balanced ledger legs to create when rule fires | legs, ledger_accounts, balance_effect, settlement_refs |
leg |
One debit or credit line created by a posting template | account_ref, side, amount_expr, currency_expr, tags |
decision |
Execution result returned to caller and logged | decision, reason_codes, posting_set_id, publication_refs |
publication_contract |
Downstream event shape for consumers | schema_version, topic, mandatory fields, latency_objective |
Example rule package¶
package: ledger-core
version: 1.0.0
effective_from: 2026-04-01T00:00:00Z
rules:
- rule_code: CARD_AUTH_HOLD
event_type: card.auth.approved
idempotency_scope: event_id
predicates:
- account.state in [ACTIVE]
- amount > 0
- available_balance >= amount
posting_template:
posting_set_type: pending_hold
legs:
- account_ref: customer.available_holds
side: debit
amount_expr: event.amount
currency_expr: event.currency
- account_ref: bank.pending_settlement
side: credit
amount_expr: event.amount
currency_expr: event.currency
decision:
status: approved
reason_codes:
- code: AUTH_HOLD_CREATED
human_text: "Card authorisation hold created"
publish:
event_name: ledger.pending_hold.created
schema_version: 1
- rule_code: MONTHLY_ACCOUNT_FEE
event_type: account.fee.monthly
idempotency_scope: event_id
predicates:
- account.state in [ACTIVE, RESTRICTED]
- product.fee_plan == STANDARD
posting_template:
posting_set_type: posted_fee
legs:
- account_ref: customer.liability
side: debit
amount_expr: event.amount
currency_expr: account.currency
- account_ref: bank.fee_income
side: credit
amount_expr: event.amount
currency_expr: account.currency
decision:
status: posted
reason_codes:
- code: FEE_MONTHLY_CHARGED
human_text: "Monthly account fee charged"
on_failure:
suspense_account: bank.fee_suspense
alert_code: FEE_POSTING_FAILURE
Execution flow¶
- Receive event and validate event schema.
- Resolve rule package by domain, product, jurisdiction, and effective date.
- Enforce idempotency before rule evaluation.
- Evaluate predicates; if none match, return a controlled rejection with reason codes.
- Build posting template and validate that total debits equal total credits.
- Execute posting set atomically.
- Update derived balances and account state side effects.
- Write immutable transaction-log records.
- Publish downstream events using the governed publication contract.
- Return decision object with reason codes, posting-set identifier, and any human-review flags.
Required decision payload¶
| Field | Requirement |
|---|---|
decision_status |
approved | posted | rejected | reversed | routed_to_suspense |
reason_codes |
At least one machine-readable code and one human-readable text |
posting_set_id |
Stable immutable identifier for the generated posting set |
affected_balances |
Current / available / pending deltas where relevant |
rule_code and package_version |
Required for audit, support, and rollback |
publication_refs |
IDs of downstream events emitted |
Mandatory guardrails¶
- No rule may create an unbalanced posting set.
- No rule may write directly to a historical posting.
- Every reversal must link to the original posting set.
- No schema-breaking publication change may be deployed without a version increment.
- Every rule promotion must include simulation evidence against historical event samples.
- Any rule failure after event acceptance must result in explicit rejection, compensating action, or suspense routing.
Suggested initial rule packs¶
| Pack | Coverage |
|---|---|
| Deposit and withdrawal posting pack | Inbound credits, outbound debits, account-to-account |
| Card authorisation, presentment, reversal, and expiry pack | Full card hold lifecycle |
| Account fee and fee waiver pack | Monthly/annual fees, fee waivers, fee reversal |
| Interest accrual and capitalisation pack | Daily accrual, capitalisation events |
| FX conversion and cross-border wallet pack | Cross-border debits, matched currency legs |
| Account closure and dormancy pack | Zero-balance closure, dormancy state transitions |
Governance¶
Rule packages are version-controlled artefacts owned by SD01 (Core Banking). Promotions through environments follow the standard deployment pipeline. Rule changes that affect posted balance types or published event shapes require ADR review before promotion.
Related policies: PAY-007 · DT-012 · OPS-007
Related architecture: Data contracts and decision publication · ADR-036