Prompt
I'm trying to build an integration that syncs invoices, payments, and ledger entries into accounting software. What architecture should I use?
Latest observation
A good default architecture for syncing invoices, payments, and ledger entries into accounting software is an event-driven, idempotent sync pipeline with a canonical internal model and a per-accounting-system adapter layer.
Recommended architecture
1) Source systems -> canonical event model
In your own platform, model accounting-relevant business events in a normalized way, such as:
InvoiceCreatedInvoiceUpdatedPaymentReceivedPaymentAppliedLedgerEntryPostedLedgerEntryReversedCustomerUpdatedTaxCodeChanged
Convert source-system data into a canonical accounting domain model before syncing outward.
Why:
- avoids coupling your business logic to any one accounting API
- makes multi-destination support much easier
- lets you enforce consistent accounting rules in one place
2) Event bus / queue for async processing
Publish those canonical events to a message queue or event bus:
- SQS / SNS
- RabbitMQ
- Kafka
- Pub/Sub
- Azure Service Bus
Use workers to process sync jobs asynchronously.
Why:
- accounting APIs are often rate-limited and flaky
- lets you retry safely
- decouples business transactions from external integrations
- handles bursts cleanly
3) Integration service with adapter pattern
Build one integration service, with a separate adapter per accounting platform:
- QuickBooks adapter
- Xero adapter
- NetSuite adapter
- Sage adapter
- etc.
Each adapter maps canonical objects to the target API payloads and handles:
- auth
- field mapping
- retries
- pagination
- rate limiting
- API quirks
- version differences
This keeps vendor-specific logic isolated.
4) Persist sync state and mappings
Store a durable sync state table for every object you export:
- internal object ID
- external system object ID
- external system name
- sync status
- last synced version/timestamp
- retry count
- error message
- hash/checksum of last exported payload
This is critical for:
- idempotency
- updates vs creates
- detecting divergence
- replaying failed jobs
- preventing duplicates
5) Use idempotent, versioned writes
Every sync operation should be safe to run multiple times.
Practical techniques:
- use external IDs / references where the accounting system supports them
- deduplicate by internal object ID + version
- use payload hashes to skip no-op updates
- implement optimistic concurrency where possible
- store a version or event sequence number
This matters especially for invoices and payments, where duplicates are painful.
6) Separate accounting workflows by object type
Invoices, payments, and ledger entries often have different sync semantics:
Invoices
- usually created before payment
- may be updated multiple times
- may require customer, tax, and line-item mappings
Payments
- usually depend on invoice existence
- may need to be applied to one or more open invoices
- often need settlement status and method mapping
Ledger entries
- often journal entries or GL postings
- may need strict debit/credit balance rules
- can be sensitive to posting period and account mappings
Treat them as separate pipelines sharing common infrastructure.
Suggested logical components
API / ingestion layer
Receives business events or sync requests from your app.
Canonical transformation layer
Normalizes data into internal accounting objects.
Validation engine
Checks:
- required fields
- account mappings
- tax rules
- currency consistency
- posting period rules
- balancing debits/credits
Job queue
Buffers sync work.
Sync workers
Execute create/update/delete/reconcile operations.
Adapter layer
Vendor-specific API client + mapping logic.
Sync state store
Tracks status, external IDs, retries, and audit history.
Audit/logging/monitoring
Captures:
- payloads
- responses
- failures
- replay history
- reconciliation metrics
Data flow example
- User creates an invoice in your app
- Your system emits
InvoiceCreated - Integration service validates and transforms it
- A sync job is queued
- Worker calls the accounting adapter
- Adapter creates invoice in external accounting software
- External ID is saved in sync-state table
- Audit log is written
- If payment arrives later,
PaymentReceived/PaymentAppliedtriggers another job - If ledger entries are posted, journal entry sync runs separately
Important design principles
1) Eventual consistency
Don’t try to make accounting sync synchronous unless absolutely necessary.
Use:
- async processing
- retries
- reconciliation jobs
2) Strong auditability
Accounting integrations need traceability.
Keep:
- original event
- transformed payload
- external response
- error logs
- timestamps
- user/system initiator
3) Reconciliation jobs
Have periodic jobs that compare source vs destination:
- missing invoices
- mismatched totals
- unapplied payments
- failed journal postings
This catches drift.
4) Backfill and replay support
You will eventually need to:
- resync an account
- replay historical data
- rebuild after API outages
- migrate to a new accounting platform
Design for reprocessing from day one.
5) Tenant isolation
If this is multi-tenant, isolate:
- credentials
- queues
- rate limits
- sync state
- audit logs
Per-tenant throttling is especially important.
When to use CDC vs application events
If your source of truth is your own database:
- Application events are usually best for business semantics
- Change Data Capture (CDC) can work if you need low-level DB change detection
- Often the best approach is: business events first, CDC as fallback
For accounting sync, business events usually map more cleanly than raw row changes.
Recommended starting architecture
If you’re early-stage, start with:
- canonical accounting model
- outbox pattern in your app
- queue-based worker processing
- per-accounting-provider adapters
- sync-state table
- retries + dead-letter queue
- reconciliation job
This gives you a scalable path without overengineering.
Common pitfalls to avoid
- syncing directly from transactional code paths
- hardcoding logic to one accounting provider
- not storing external IDs
- not handling retries/idempotency
- assuming invoice/payment/ledger objects can share the same workflow
- ignoring timezone, currency, and tax edge cases
- no reconciliation/audit trail
- treating accounting APIs as reliable or fast
A concise reference architecture
Source app
→ Outbox/event publisher
→ Queue/event bus
→ Validation + canonical model service
→ Provider adapter workers
→ Accounting software API
→ Sync state + audit log
→ Reconciliation/replay jobs
If you want, I can also provide:
- a diagram of this architecture,
- a database schema for sync state and audit logs, or
- a sample implementation in Node.js, Python, or Java.