A production-ready microservices-based banking application built with .NET 8, implementing the SAGA pattern for distributed transactions.
This project demonstrates a complete microservices architecture with:
- 4 Microservices: Identity, Account, Transfer (SAGA Orchestrator), Notification
- Event-Driven Communication: MassTransit with RabbitMQ message broker
- Database-Per-Service: 3 PostgreSQL databases
- SAGA Pattern: Orchestration-based distributed transactions with compensation
- Containerization: Docker & Docker Compose
-
Identity Service (Port 5001)
- User registration & authentication
- JWT token generation
- PostgreSQL database
-
Account Service (Port 5002)
- Bank account management
- Balance operations (debit/credit)
- Event-driven balance updates
- PostgreSQL database
-
Transfer Service (Port 5003)
- SAGA orchestrator for money transfers
- Distributed transaction coordination
- Compensation logic for failures
- PostgreSQL database
-
Notification Service (Port 5004)
- Asynchronous event consumer
- Transfer success/failure notifications
- Stateless service
git clone https://github.com/batuhansimsar/Microbank.git
cd Microbankdocker-compose up -dThis will start:
- 3 PostgreSQL databases (ports 5532, 5533, 5534)
- RabbitMQ (port 5672, management UI on 15672)
- 4 microservices (ports 5001-5004)
docker-compose psAll containers should show "Up" status.
curl -X POST http://localhost:5001/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "SecurePass123!",
"fullName": "John Doe"
}'curl -X POST http://localhost:5001/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "SecurePass123!"
}'Save the token from the response.
TOKEN="your-jwt-token-here"
curl -X POST http://localhost:5002/api/accounts \
-H "Authorization: Bearer $TOKEN"Save the account id from the response.
ACCOUNT_ID="your-account-id-here"
curl http://localhost:5002/api/accounts/$ACCOUNT_ID/balance \
-H "Authorization: Bearer $TOKEN"Repeat the registration, login, and account creation steps for a second user.
FROM_ACCOUNT="first-account-id"
TO_ACCOUNT="second-account-id"
curl -X POST http://localhost:5003/api/transfers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"fromAccountId\": \"$FROM_ACCOUNT\",
\"toAccountId\": \"$TO_ACCOUNT\",
\"amount\": 100,
\"currency\": \"TRY\"
}"TRANSFER_ID="transfer-id-from-response"
curl http://localhost:5003/api/transfers/$TRANSFER_ID \
-H "Authorization: Bearer $TOKEN"- Start Infrastructure Only
docker-compose up -d identity-db account-db transfer-db rabbitmq- Run Each Service
# Terminal 1 - Identity Service
cd src/Services/Identity/Identity.API
dotnet run --urls="http://localhost:5001"
# Terminal 2 - Account Service
cd src/Services/Account/Account.API
dotnet run --urls="http://localhost:5002"
# Terminal 3 - Transfer Service
cd src/Services/Transfer/Transfer.API
dotnet run --urls="http://localhost:5003"
# Terminal 4 - Notification Service
cd src/Services/Notification/Notification.API
dotnet run --urls="http://localhost:5004"Microbank/
├── src/
│ ├── BuildingBlocks/
│ │ ├── Common/ # Shared utilities
│ │ ├── Common.Logging/ # Serilog configuration
│ │ └── EventBus.RabbitMQ/ # Event bus implementation
│ │
│ └── Services/
│ ├── Identity/
│ │ ├── Identity.API/
│ │ └── Dockerfile
│ ├── Account/
│ │ ├── Account.Domain/
│ │ ├── Account.API/
│ │ └── Dockerfile
│ ├── Transfer/
│ │ ├── Transfer.Domain/
│ │ ├── Transfer.API/
│ │ └── Dockerfile
│ └── Notification/
│ ├── Notification.API/
│ └── Dockerfile
│
├── docker-compose.yml
├── Microbank.sln
└── README.md
The Transfer Service implements a centralized orchestration-based Saga pattern using MassTransit State Machine to manage distributed transactions across Account service boundaries.
Orchestration-based (Current):
- ✅ Centralized state machine in Transfer service
- ✅ All orchestration logic in
TransferStateMachine - ✅ Saga state persisted to database (
TransferSagaStatestable) - ✅ Easy to monitor, debug, and extend
- ✅ Automatic compensation on failures
Previous Choreography approach:
- Services communicated directly via events
- No central state tracking
- Logic scattered across event handlers
[Transfer Initiated]
↓
[Debiting] → Publish IDebitAccountRequested
↓
├─→ [Success: AccountDebited]
│ ↓
│ [Crediting] → Publish ICreditAccountRequested
│ ↓
│ ├─→ [Success: AccountCredited] → ✅ COMPLETED
│ │
│ └─→ [Failure: Credit Failed]
│ ↓
│ [Compensating] → Refund sender
│ ↓
│ ❌ FAILED (with compensation)
│
└─→ [Failure: Debit Failed] → ❌ FAILED (no compensation needed)
| State | Description |
|---|---|
Initiated |
Transfer created, ready to debit sender account |
Debiting |
Waiting for debit confirmation from Account service |
Debited |
Sender debited successfully, ready to credit receiver |
Crediting |
Waiting for credit confirmation from Account service |
Completed |
Transfer successful - both debit and credit completed ✅ |
Compensating |
Rolling back - refunding sender after credit failure |
Failed |
Transfer failed (with or without compensation) ❌ |
- User initiates transfer → Saga state:
Initiated - Saga requests debit → Publishes
IDebitAccountRequested→ State:Debiting - Account service debits sender → Publishes
IAccountDebited→ State:Debited - Saga requests credit → Publishes
ICreditAccountRequested→ State:Crediting - Account service credits receiver → Publishes
IAccountCredited→ State:Completed✅ - Notification sent → Transfer complete
- User initiates transfer → Saga state:
Initiated - Saga requests debit → State:
Debiting - Sender account debited → State:
Debited - Saga requests credit → State:
Crediting - Credit fails (e.g., receiver account closed) → Publishes
IAccountOperationFailed - Saga triggers compensation → Publishes
ICompensateDebit→ State:Compensating - Account service refunds sender → Money returned to original account
- Transfer marked as failed → State:
Failed❌ - Failure notification sent
The saga state is persisted in the TransferSagaStates table:
-- View all saga states
SELECT
"CorrelationId",
"CurrentState",
"TransferId",
"Amount",
"InitiatedAt",
"CompletedAt",
"FailureReason"
FROM "TransferSagaStates"
ORDER BY "InitiatedAt" DESC;
-- Find stuck/pending transfers
SELECT * FROM "TransferSagaStates"
WHERE "CurrentState" NOT IN ('Completed', 'Failed')
AND "InitiatedAt" < NOW() - INTERVAL '5 minutes';- Automatic Compensation: If credit fails after successful debit, saga automatically refunds sender
- State Persistence: Survives service restarts - saga continues from last known state
- Idempotency: Inbox/Outbox pattern ensures exactly-once message processing
- Observability: Query database to see exact state of any transfer
- Retry Policies: Configured with 3 retries at 5-second intervals
- Concurrency Control: Pessimistic locking prevents race conditions
POST /api/auth/register- Register new userPOST /api/auth/login- Login and get JWT tokenGET /api/auth/me- Get current user info
POST /api/accounts- Create bank accountGET /api/accounts/{id}- Get account detailsGET /api/accounts/{id}/balance- Get balanceGET /api/accounts/{id}/transactions- Get transaction history
POST /api/transfers- Initiate transferGET /api/transfers/{id}- Get transfer statusGET /api/transfers/user/{userId}- Get user's transfers
- No HTTP endpoints (event-driven only)
Each service connects to its own PostgreSQL database:
- Identity:
localhost:5532 - Account:
localhost:5533 - Transfer:
localhost:5534
- AMQP:
localhost:5672 - Management UI:
http://localhost:15672(guest/guest)
Configured in each service's appsettings.json:
- SecretKey: Change in production!
- Issuer:
MicrobankIdentity - Audience:
MicrobankServices
# Stop all containers
docker-compose down
# Stop and remove volumes (WARNING: deletes all data)
docker-compose down -v- Backend: .NET 8, ASP.NET Core
- Database: PostgreSQL 16
- Message Broker: RabbitMQ 3
- Service Bus: MassTransit 8
- ORM: Entity Framework Core 8
- Logging: Serilog
- Authentication: JWT Bearer
- Containerization: Docker
MIT License