A production-patterned .NET 8 REST API demonstrating core fintech backend engineering concerns. Built as a public portfolio proxy for confidential payments infrastructure work delivered at Access Bank Plc.
| Concern | Implementation |
|---|---|
| Idempotent transfers | Caller-supplied key, 24hr expiry, DB unique index backstop |
| Async processing | Hosted BackgroundService polls and processes pending transfers |
| Atomic debit/credit | Single SaveChangesAsync under EF Core transaction |
| Cross-currency FX | IExchangeRateService interface — stub in dev, swappable for real provider |
| Webhook delivery | HMAC-SHA256 signed POSTs with exponential backoff retry |
| Audit trail | Separate AuditDbContext — writes survive business transaction rollbacks |
| Error handling | Single middleware, RFC 7807 Problem Details, named exception hierarchy |
| Crash recovery | Worker resets stale Processing transfers on startup |
┌─────────────────────────────────────────────────────┐
│ PayCore.Api │
│ │
│ Features/ Infrastructure/ │
│ ├── Accounts/ ├── Data/ │
│ │ ├── AccountsController │ ├── PayCoreDbContext │
│ │ └── AccountService │ └── AuditDbContext │
│ ├── Transfers/ ├── Workers/ │
│ │ ├── TransfersController│ ├── TransferProcessor│
│ │ ├── TransferService │ └── WebhookDispatch │
│ │ └── ExchangeRates/ ├── Audit/ │
│ └── Webhooks/ ├── Security/ │
│ ├── WebhooksController ├── Middleware/ │
│ └── WebhookService └── Exceptions/ │
└─────────────────────────────────────────────────────┘
Two DbContexts — one key design decision:
PayCoreDbContext owns business data and participates in transactions.
AuditDbContext is never enlisted — audit records exist even when
business operations fail or roll back.
- .NET 8, C#, ASP.NET Core Web API
- Entity Framework Core 8 + MSSQL
- FluentValidation
- Serilog (structured logging)
- XUnit + FluentAssertions + Moq
- Swagger / OpenAPI
- .NET 8 SDK
- SQL Server (LocalDB is fine for local dev)
git clone https://github.com/gravy17/paycore.git
cd paycore
# Restore packages
dotnet restore
# Apply migrations
dotnet ef database update --project src/PayCore.Api --context PayCoreDbContext
dotnet ef database update --project src/PayCore.Api --context AuditDbContext
# Run
dotnet run --project src/PayCore.ApiSwagger UI: https://localhost:{port}/swagger
dotnet test| Method | Route | Description |
|---|---|---|
POST |
/api/accounts |
Create account |
GET |
/api/accounts/{id} |
Get account + balance |
PATCH |
/api/accounts/{id} |
Deactivate account |
GET |
/api/accounts/{id}/transfers |
List transfers (filterable) |
POST |
/api/transfers |
Initiate transfer (async) |
GET |
/api/transfers/{id} |
Get transfer status |
POST |
/api/accounts/{id}/webhooks |
Register webhook |
DELETE |
/api/accounts/{id}/webhooks/{wid} |
Deactivate webhook |
All transfer requests require a caller-supplied idempotencyKey.
Duplicate requests with the same key return the original transfer unchanged (200 OK).
Keys expire after 24 hours — replays after expiry return 422.
This mirrors the behaviour of production payment processors (Stripe, Paystack).
Outbound webhooks include an X-PayCore-Signature: sha256={hex} header.
Receivers should verify this using HMAC-SHA256 with the shared secret:
var expectedSig = "sha256=" +
Convert.ToHexString(HMACSHA256.HashData(
Encoding.UTF8.GetBytes(secret),
Encoding.UTF8.GetBytes(payload)
)).ToLowerInvariant();
var isValid = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSig),
Encoding.UTF8.GetBytes(receivedSignature));This project intentionally omits authentication/authorisation to keep the focus on payments domain patterns. A production deployment would add:
- JWT bearer auth (Azure AD B2C or similar)
- Per-tenant account isolation
- Real FX rate provider (e.g. Open Exchange Rates)
- Outbox pattern for guaranteed webhook delivery
- Azure Service Bus for transfer queue (replacing the polling worker)