The Financial Core
Payments and the general ledger rebuilt as one event-driven backbone — 500K+ transactions a day, and a period-end close that fell from five days to under eight hours.
Client
Multi-Tenant Enterprise ERP Platform
Industry
Enterprise Finance · ERP Platform Engineering
Duration
18 months
Year
2025
Background
In most ERP platforms, money movement and the ledger are two separate systems that agree with each other once a month. Payments run in one module, the general ledger runs in another, and the gap between them is closed by an overnight batch job and a finance team with a spreadsheet. This platform was no different: sub-ledger data from payables, receivables, payroll and inventory reached the GL through jobs that ran after midnight, so balances were always a day stale, and every period-end close opened with a hunt for what had not posted. Closing a period took five days.
The Challenge
Rebuilding either half alone would not have helped. A faster payment engine still hands the ledger a pile of transactions to reconcile later; a faster ledger still waits on the batch. The two had to become one system — which meant solving the hard parts of distributed software on a path where the ordinary failure modes are unacceptable. A retried request must never become a second payment. A consumer that falls behind must never silently drop a journal posting. And the output is audited, so every entry has to carry enough evidence to reconstruct who changed what, when, and why — months after the fact.
Our Solution
SageWare rebuilt payments and the general ledger as a single event-driven backbone. The payment engine models the invoice-to-pay lifecycle — scheduling, approval, execution across rails, bulk file handling — and publishes domain events rather than writing to the ledger directly. The GL posting engine consumes those events alongside sub-ledger activity from AP, AR, payroll and inventory, and generates double-entry journal entries in real time against configurable mapping rules, with multi-entity, multi-currency and multi-book support. Because the ledger posts continuously as payments settle, reconciliation stops being a month-end event and becomes a property of the system. Period-end work that genuinely is batch-shaped — trial balance, revaluation, intercompany elimination — stayed batch, and every modification anywhere in the chain lands in an append-only audit store.
Key Deliverables
- Payments and the general ledger as one event-driven backbone rather than two reconciled systems
- Exactly-once payment execution via caller-supplied reference IDs and distributed locking
- Real-time double-entry posting from AP, AR, payroll and inventory sub-ledgers
- Multi-entity, multi-currency and multi-book ledger with configurable account mapping
- Period-end automation for trial balance, revaluation and intercompany elimination
- Append-only audit store capturing user, timestamp, reason and before/after values
Results & Impact
How We Did It
01
Domain & Event Modelling
Modelled the invoice-to-pay lifecycle and sub-ledger activity as domain events, then fixed service boundaries, data contracts and an API versioning strategy before any code was written.
02
Ledger Architecture
Designed the chart of accounts model, multi-entity and multi-book structures, and the configurable mapping rules translating sub-ledger transactions into GL accounts.
03
Payment Engine & Idempotency Core
Built the payment engine with exactly-once semantics on the send path, integrated rails over ISO 20022, and added Spring Batch jobs for SFTP bulk payment files with per-format parsers.
04
Real-Time Posting Engine
Built the GL consumer generating double-entry journals in real time with multi-currency handling, plus the control-account reconciliation engine and its automated discrepancy alerting.
05
Period-End Automation & Controls
Automated trial balance, revaluation and intercompany elimination on batch, and delivered the append-only audit store and role-based GL inquiry APIs.
Decisions & Trade-offs
Events instead of the nightly batch — and why that was the whole point
Moving sub-ledger data onto an event backbone rather than an overnight job is what collapsed the close. The close did not get faster because the batch got faster; it got faster because a ledger that posts continuously has almost nothing left to reconcile at month end. The cost is that ordering, replay and consumer lag become our problem rather than the scheduler’s — which is why event contracts are versioned and every topic is ordered and replayable.
Idempotency keys and a short distributed lock, not a dedup table
A dedup table makes the database the arbiter of whether a payment already went out, which holds right up until the write that records the payment and the call that sends it sit in different transactions. Instead every payment carries a caller-supplied reference ID and takes a short distributed lock for the duration of the send, so a retry either finds the original or waits on it. Duplicates are prevented at the door rather than detected afterwards.
Streaming for fan-out, batch for the things that are actually batch
Kafka is the right tool for an event several independent consumers need — ledger, notifications, audit. It is the wrong tool for “revalue every account in this ledger.” Period-end closing stayed on Spring Batch because those jobs are bounded, ordered and restartable, and a half-finished revaluation needs to resume, not replay. Mixing the two deliberately was cheaper than forcing either model to cover both.
An append-only audit store, not a change log
Auditors rarely ask what a balance is. They ask what it was, who changed it, and on what authority. Capturing user, timestamp, reason and before/after values on every modification — append-only, never updated in place — turns that from an investigation into a query, and is what made the platform SOX-ready rather than merely well-logged.
Scope & Boundaries
We built the financial core, not the whole ERP. GL inquiry APIs expose account balances, trial balance and journal history behind role-based access control, and the platform’s existing front end consumes them — we did not replace the reporting layer or the UI. Tax determination and statutory filing remained with the incumbent system. Payment rail coverage is domestic; cross-border settlement was explicitly out of scope.
Next Case Study
Trading Partner EDI Integration
B2B Commerce · Document Integration
