A demonstration project showing resilience patterns for mission-critical banking systems, built with .NET 10 and orchestrated with .NET Aspire. Designed for a 55-minute conference talk.
- One-Command Start -
./start-aspire.shlaunches everything - .NET Aspire - Modern orchestration with built-in observability
- Real-World Patterns - Retry, Circuit Breaker, Outbox, Inbox, Ordering
- Shared Libraries - Reusable inbox/outbox base classes eliminate duplication
- Type-Safe Constants - No magic strings, centralized configuration
- Live Observability - Aspire Dashboard + Jaeger tracing
- Chaos Testing - Dev Proxy for failure injection
- Production-Ready - Patterns used in actual banking systems
βββββββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
β Payments API ββββββββββΆβ Dev Proxy ββββββββββΆβ Core Bank API β
β (Your Service) β β (Chaos) β β (Legacy SaaS) β
βββββββββββββββββββ ββββββββββββββββ βββββββββββββββββββ
β β
β Outbox Pattern β Inbox Pattern
βΌ βΌ
βββββββββββ βββββββββββ
β SQLite β β SQLite β
βββββββββββ βββββββββββ
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Both send traces to
β
βΌ
ββββββββββββ
β Jaeger β
ββββββββββββ
# Start everything with .NET Aspire
cd CoreBankDemo.AppHost
aspire runThis will launch:
- Payments API (http://localhost:5294)
- Core Bank API (http://localhost:5032)
- Dev Proxy (http://localhost:8000) - Chaos engineering proxy
- PostgreSQL databases (paymentsdb, corebankdb)
- Jaeger (http://localhost:16686)
- Aspire Dashboard (http://localhost:15888)
Everything runs automatically - no manual steps needed!
- Aspire Dashboard: http://localhost:15888 (when using Aspire)
- Jaeger Tracing: http://localhost:16686
- Payments API OpenAPI: http://localhost:5294/openapi/v1.json
- Core Bank API OpenAPI: http://localhost:5032/openapi/v1.json
- Health Checks:
- Payments API: http://localhost:5294/health
- Core Bank API: http://localhost:5032/health
Goal: Show basic architecture working perfectly.
Configuration:
- All features disabled
- Direct calls to Core Bank API
Demo:
- Send payment request via
demo-requests.http - Show successful processing
- Explain architecture: Payments API β Core Bank API
Key Point: Works great when everything is perfect!
Goal: Handle transient failures.
Setup:
- Enable DevProxy: Set
"enabled": trueindevproxy.jsonforGenericRandomErrorPlugin - Restart Aspire (Ctrl+C and
dotnet runagain)- Aspire will automatically restart DevProxy with new configuration
Demo:
- Show random failures (503, 429, 500)
- Explain
AddStandardResilienceHandler()inProgram.cs:17 - Show retries in logs
- Open Jaeger and show:
- Multiple HTTP spans for retries
- Latency measurements
- Success after retries
What's included:
- Exponential backoff retry
- Circuit breaker
- Timeout policies
Code Reference: CoreBankDemo.PaymentsAPI/Program.cs:17
Key Point: Handles ~95% of real-world transient issues.
Goal: Handle longer outages without losing requests.
Configuration:
Already enabled in appsettings.Development.json:
"Features": {
"UseOutbox": true
}Demo:
- Keep DevProxy error rate high or stop Core Bank API in Aspire Dashboard
- Send payment requests
- Show 202 Accepted response with "Pending" status
- Query outbox:
GET http://localhost:5294/api/outbox - Show messages stored in PostgreSQL (paymentsdb)
- Restart Core Bank API in Aspire Dashboard or reduce DevProxy errors
- Watch OutboxProcessor logs in Aspire Dashboard - see automatic retry
- Query outbox again - show "Completed" status
How it works:
- Payment requests stored in local database
- Background service (
OutboxProcessor.cs) polls every 5 seconds - Retries failed messages up to 5 times
- Eventually consistent processing
Code References:
- Outbox storage:
CoreBankDemo.PaymentsAPI/Program.cs:53-79 - Background processor:
CoreBankDemo.PaymentsAPI/OutboxProcessor.cs - Database model:
CoreBankDemo.PaymentsAPI/OutboxMessage.cs
Key Point: Don't lose customer requests! But this introduces new problems...
Goal: Prevent duplicate processing (idempotency).
Problem:
- Retry can cause duplicate transactions
- Customer charged twice!
Configuration:
Enable Inbox in Core Bank API appsettings.Development.json:
"Features": {
"UseInbox": true
}Demo:
- Show idempotency key in transaction request
- Manually send same transaction twice:
POST http://localhost:5032/api/transactions/process { "fromAccount": "NL91ABNA0417164300", "toAccount": "NL20INGB0001234567", "amount": 100.00, "currency": "EUR", "idempotencyKey": "test-123" }
- Query inbox:
GET http://localhost:5032/api/inbox - Show same
transactionIdreturned for duplicate - Explain: first request creates transaction, second returns cached result
How it works:
- Core Bank API stores processed requests with idempotency key
- Duplicate requests return original response
- No duplicate charges
Code Reference: CoreBankDemo.CoreBankAPI/Program.cs:36-90
Key Point: Critical for financial systems - exactly-once processing.
Goal: Maintain per-account ordering while scaling.
Problem:
- Multiple payments from same account processed out of order
- Balance calculations can be wrong
- Race conditions
Configuration:
Already enabled in appsettings.Development.json:
"Features": {
"UseOrdering": true
}Demo:
- Create multiple payments from same account quickly
- Show
PartitionKeyin outbox (set toFromAccount) - Explain processing logic:
- One message per partition at a time
- Multiple partitions processed concurrently
- Ordering preserved within each account
- Show logs: messages from different accounts processed in parallel
How it works:
- Each message partitioned by
FromAccount - Processor takes oldest message per partition
- Sequential processing per account
- Parallel processing across accounts
Code References:
- Partition key:
CoreBankDemo.PaymentsAPI/Program.cs:73 - Ordering logic:
CoreBankDemo.PaymentsAPI/OutboxProcessor.cs:44-79
Key Point: Balance scalability with ordering guarantees.
Tools that help:
- .NET Aspire: Orchestration and observability (see ASPIRE.md)
- Dev Proxy: Chaos testing in development
- Jaeger: Distributed tracing and observability
- DevContainer: Consistent development environment
- Entity Framework: Simple persistence
- OpenTelemetry: Standard instrumentation
Pattern Layering:
- Retry/Circuit Breaker: First line of defense (transient failures)
- Outbox: Second line (sustained outages)
- Inbox: Data integrity (idempotency)
- Ordering: Business logic guarantees (per-entity consistency)
Key Takeaways:
- Resilience is layered - no single solution
- Observability is not optional
- Test failure scenarios in development
- Tools exist - don't build everything from scratch
Control patterns via appsettings.json:
"Features": {
"UseOutbox": false, // Store-and-forward for outages
"UseInbox": false, // Idempotency/deduplication
"UseOrdering": false // Per-account ordering
}Valid accounts in Core Bank API:
NL91ABNA0417164300NL20INGB0001234567NL39RABO0300065264
Edit devproxy.json:
{
"name": "GenericRandomErrorPlugin",
"enabled": true // Set to true
}{
"name": "LatencyPlugin",
"enabled": true // Set to true
}{
"name": "RateLimitingPlugin",
"enabled": true // Set to true
}paymentsdb- Payments API outbox and inbox (PostgreSQL)corebankdb- Core Bank API inbox and messaging outbox (PostgreSQL)
To reset state, delete the database containers or clear the tables.
The load test configuration uses a hardcoded Redis password (myredispassword123) in the following files:
CoreBankDemo.LoadTests/AppHost.csdapr/components/lockstore-redis.yamldapr/components/pubsub-redis.yamldapr/components-loadtest/lockstore-redis.yamldapr/components-loadtest/pubsub-redis.yaml
This is intentional β the Redis instance is disposable and local-only, spun up and torn down by Aspire for each load test run. The password has no security implications outside that ephemeral container. Do not use these credentials for any real environment.
DevProxy not working?
# Ensure the devproxy executable is in the project root
./devproxy --help
# Or check the devproxy.json configuration filePort already in use?
lsof -ti:5032 | xargs kill # Core Bank API
lsof -ti:5294 | xargs kill # Payments API
lsof -ti:8000 | xargs kill # Dev ProxyJaeger not showing traces?
- Check
OTEL_EXPORTER_OTLP_ENDPOINTinappsettings.json - Ensure docker compose is running:
docker compose ps
Database errors?
# Clear PostgreSQL databases via Aspire Dashboard or restart with clean volumes
# Databases are automatically created on startupThe project includes comprehensive load tests that validate the system under concurrent load:
# Run load tests with k6
dotnet run --project CoreBankDemo.LoadTestsWhat it tests:
- Exactly-once processing under concurrent load (10 VUs submitting 1000+ transactions)
- Idempotency: ~10% are deliberate retry attempts with duplicate idempotency keys
- End-to-end flow: Payments API outbox β Core Bank API inbox β transaction processing
- No failed messages, no pending messages, no duplicate processing
Configuration: Edit CoreBankDemo.LoadTests/appsettings.json:
{
"LoadTest": {
"TransactionCount": "1000", // Total unique transactions
"VuCount": "10" // Concurrent virtual users
}
}The load test uses disposable PostgreSQL and Redis instances, seeded with 10 test accounts (β¬10M each). See CoreBankDemo.LoadTests/README.md for details.
For detailed architecture information, database schemas, and implementation details, see:
- ARCHITECTURE.md - Complete technical architecture documentation
- Shared library design (CoreBankDemo.Messaging)
- Pattern implementations (Inbox/Outbox/Partitioning)
- Database schemas
- Configuration options
- Design decisions and rationale
- Load testing strategy