Anti-Corruption Layer (ACL)
v0.1.0Decoupled cross-domain ingestion from Billing, WMS, POS, and CRM into balanced General Ledger postings without operational coupling.
Anti-Corruption Layer (ACL)
In enterprise Domain-Driven Design (DDD), operational systems—such as Sales, Warehouse Management (WMS), Invoicing, and Point of Sale (POS)—must never write directly to accounting database tables. Doing so couples business logistics to statutory ledger models.
The Anti-Corruption Layer (ACL) (ium()->accounting()->acl()) acts as a protective boundary that translates upstream domain events into balanced, zero-sum double-entry vouchers.
1. Architectural Boundary Isolation
flowchart TD
subgraph OperationalDomains["Operational Bounded Contexts"]
POS["POS / Invoicing<br/>InvoiceSettledEvent"]
WMS["Warehouse (WMS)<br/>StockWritedownEvent"]
PUR["Purchasing<br/>GoodsReceivedEvent"]
end
subgraph ACLBoundary["Anti-Corruption Layer (ACL)"]
INGEST["ium()->accounting()->acl()->ingest($dto)"]
MAP["Event-Account Mapping Registry<br/>Resolves DR / CR Chart Codes"]
VAL["Validation & Minor-Unit Math<br/>Asserts Σ Debits === Σ Credits"]
end
subgraph AccountingCore["General Ledger Core"]
JV["Posted Journal Voucher"]
GL["Immutable GL Postings"]
AUD["Forensic Audit Log"]
end
OperationalDomains -->|Domain Event Payload| INGEST
INGEST --> MAP
MAP --> VAL
VAL --> AccountingCore
2. Registering Event Account Mappings
Before ingesting domain facts, register which Chart of Accounts codes handle each event type:
// Map customer checkout event to Cash Debit and Sales Revenue Credit
ium()->accounting()->acl()->registerMapping(
eventName: 'billing.invoice_settled',
debitAccountCode: '1111', // Bank Operating Checking
creditAccountCode: '4100' // Product Sales Revenue
);
// Map inventory write-down event to Cost of Goods Sold and Inventory Asset
ium()->accounting()->acl()->registerMapping(
eventName: 'inventory.stock_writedown',
debitAccountCode: '5120', // Inventory Shrinkage & Loss
creditAccountCode: '1310' // Finished Goods Inventory
);
3. Ingesting Cross-Domain Facts
When an upstream event occurs, dispatch an AclIngestEventDto:
use Obelaw\Ium\Accounting\Data\AclIngestEventDto;
// Called from an event listener or background queue worker
$voucher = ium()->accounting()->acl()->ingest(
new AclIngestEventDto(
eventName: 'billing.invoice_settled',
amountMinor: 850000, // $8,500.00 stored in integer cents
currencyCode: 'USD',
transactionDate: '2026-03-31',
description: 'Customer INV-9821 settled via Stripe Checkout',
sourceType: 'billing.invoice',
sourceId: '9821'
)
);
echo "Automated Voucher Created: {$voucher->voucher_number}\n";
echo "Voucher Status: {$voucher->status}\n"; // POSTED immediately
4. Key Guarantees Enforced by the ACL
- Zero Database Coupling: The billing module has zero foreign keys to
ium_accounting_*tables. It only passes its ownsourceTypeandsourceId. - Idempotency Protection: Ingesting the same
sourceTypeandsourceIdcombination a second time will detect the existing voucher and safely return it without double-posting. - Audit Cross-Reference: The resulting
JournalEntryand audit logs record the originating event name, source model, and source ID, allowing statutory auditors to trace from financial reports back to the operational document in one click.