Event-Driven Architecture Patterns
Mental models for building event-driven systems. Understanding event sourcing, CQRS, eventual consistency, and when to use them in fintech contexts.
Why Events Matter in Financial Systems
Financial systems are inherently event-driven. A payment succeeds, a loan defaults, a borrower updates their address. These aren’t just state changes, they’re business events with meaning.
Traditional CRUD thinking treats these as database updates. Event-driven thinking treats them as first-class domain events that trigger downstream processes. The difference matters when you’re coordinating across multiple systems.
Events vs Commands vs State Changes
Not everything should be an event. Here’s how to think about it:
Commands are requests for action. “ProcessPayment”, “ApproveLoan”, “SendNotification”. They can fail. They express intent.
Events are facts about what happened. “PaymentProcessed”, “LoanApproved”, “NotificationSent”. They can’t fail because they already happened. They express reality.
State changes are the mutations to your database. They’re the implementation detail, not the interface.
In an event-driven architecture, commands trigger business logic that produces events. Events trigger state changes and downstream processes. This separation makes the system easier to reason about and test.
Event Sourcing: When and Why
Event sourcing means storing events as your source of truth rather than current state. Your balance isn’t a number in a database, it’s the sum of all deposit and withdrawal events.
This sounds elegant until you try to query it. “Show me all loans with a balance over $10k” becomes expensive when you have to replay events for every loan.
Use event sourcing for:
- Audit trails (financial systems need complete history)
- Complex state machines (loan lifecycle, collections workflows)
- Temporal queries (“what was the state on this date?”)
Don’t use it for:
- Simple CRUD entities
- Data that gets queried more than it changes
- High-volume, low-value events
CQRS: Read and Write Separation
Command Query Responsibility Segregation means your write model and read model are different.
You write events to an append-only log. You read from projections (materialized views) optimized for queries. The projection can be relational, document, search index. Whatever fits the read pattern.
For lending systems, this works well:
- Write: Loan events (originated, payment made, defaulted)
- Read projections: Current loan state, borrower dashboard, collections queue
The projections are eventually consistent. That’s fine for most reads. For the few that need strong consistency, query the write model directly.
Handling Eventual Consistency
Eventual consistency breaks intuitions. A borrower makes a payment but the balance doesn’t update immediately. How do you handle that in the UI?
Options:
- Optimistic UI: Show the expected state immediately, with a spinner.
- Lag indicator: “Balance as of 2 minutes ago. Updating…”
- Synchronous path: For critical operations, wait for the projection to update.
We use different strategies for different operations. Payment confirmation? Optimistic. Account dashboard? Lag indicator. Loan approval? Synchronous.
When Not to Go Event-Driven
Event-driven architecture adds complexity. You’re trading simple database transactions for distributed systems problems. Only worth it if:
- You need to coordinate across multiple services
- You need complete audit logs
- You have complex workflows with many decision points
- Your domain is naturally event-driven (it is in fintech)
If you’re building a simple CRUD app, stick with CRUD. Don’t add events just because they’re fashionable.