Multi-service payment backend for African markets: mobile money, cards (via PSPs), bank transfers, wallets, double-entry ledger, webhooks, subscriptions, and payouts. PSP integrations use an adapter layer (Flutterwave, Paystack, CinetPay) with routing, failover, and Resilience4j circuit breakers.
| Layer | Technology |
|---|---|
| Runtime | Java 17 (Spring Boot parent); use JDK 21 in production if desired |
| Framework | Spring Boot 3.4, Spring Cloud 2024 |
| API edge | Spring Cloud Gateway (rate limiting via Redis) |
| Data | PostgreSQL (per-service schemas + Flyway), Redis |
| Messaging | Kafka |
| Observability | Actuator, Micrometer, Prometheus, Grafana (see Monitoring) |
| API docs | springdoc OpenAPI (Swagger UI per service) |
- Architecture — sequence-style flows, Kafka topics, idempotency, orchestrator routing, failure modes
- Operations — environment variables, dependencies, scaling, backups, CI, runbook prompts
- Contributing — Maven modules, adding a PSP adapter, database migrations
- Monitoring — Prometheus, Grafana, metrics endpoints
Services (ports are local defaults):
| Service | Port | Role |
|---|---|---|
api-gateway |
8080 | Routes, JWT validation, Redis rate limits |
auth-service |
8081 | Users, merchants, roles, JWT issuance (/auth context path) |
payment-service |
8082 | Payment intents, idempotency, Kafka events, orchestrator client |
gateway-orchestrator-service |
8083 | PaymentGateway adapters, smart routing, circuit breaker (/orchestrator) |
wallet-service |
8084 | Balances, freeze/unfreeze |
ledger-service |
8085 | Double-entry journal posting |
webhook-service |
8086 | HMAC verification, dedupe, Kafka |
subscription-billing-service |
8087 | Plans, subscriptions, billing run |
payout-service |
8088 | Payout batches |
- Client
POST /payments(through gateway) withAuthorization+ optionalIdempotency-Key. payment-servicepersists intent (PostgreSQLpaymentschema), calls orchestratorPOST /orchestrator/v1/chargewith the same JWT.- Orchestrator selects a gateway (currency/country + hint), returns authorization URL (sandbox/mock in default profile).
- PSP sends webhook →
webhook-serviceverifies signature (when secrets set), dedupes, emits Kafkapayments.webhooks. - Downstream consumers (your workers) call wallet + ledger to settle funds and post accounting entries.
- Never persist raw card numbers or CVV; use hosted fields / PSP tokens only.
AesEncryptionService(AES-256-GCM) incommon-libis for non-PCI secrets (e.g. API tokens at rest), not cards.
PostgreSQL database afripay with one schema per bounded context (each Flyway runs independently).
erDiagram
AUTH_MERCHANTS ||--o{ AUTH_USERS : "merchant_id"
AUTH_USERS ||--|{ AUTH_USER_ROLES : "roles"
PAYMENT_INTENTS {
uuid id PK
bigint amount_minor
varchar currency
uuid customer_id
varchar status
varchar idempotency_key UK
}
WALLET_WALLETS {
uuid id PK
uuid owner_id
varchar currency
bigint balance_minor
bigint frozen_minor
}
LEDGER_ACCOUNTS {
uuid id PK
varchar code UK
varchar type
varchar currency
}
LEDGER_JOURNAL_TRANSACTIONS {
uuid id PK
varchar idempotency_key UK
varchar currency
}
LEDGER_JOURNAL_TRANSACTIONS ||--|{ LEDGER_LEDGER_ENTRIES : "contains"
LEDGER_ACCOUNTS ||--|{ LEDGER_LEDGER_ENTRIES : "posted_to"
BILLING_PLANS ||--o{ BILLING_SUBSCRIPTIONS : "plan_id"
PAYOUT_BATCHES {
uuid id PK
uuid merchant_id
varchar status
}
- Payment intents: partial unique index on
idempotency_key(where not null); indexes oncustomer_id,status. - Ledger posting:
SERIALIZABLEtransaction when posting journals; immutableledger_entries(insert-only). - Wallets: optimistic locking via
@Version+ pessimistic read for debit/credit paths.
- JDK 17+, Maven 3.9+
- Docker (for Postgres, Redis, Kafka, Prometheus, Grafana)
docker compose up -d postgres redis zookeeper kafka prometheus grafanaCreate DB once (image default): user afripay / password afripay / database afripay.
- JWT: shared secret
JWT_SECRET(min 256 bits for HS256). Same value on gateway and all resource servers. - Orchestrator URL (payment → orchestrator):
ORCHESTRATOR_URL=http://localhost:8083/orchestrator - PSP secrets (webhooks):
FLW_WEBHOOK_SECRET,PAYSTACK_WEBHOOK_SECRET,CINETPAY_WEBHOOK_SECRET(empty = dev mode accepts unsigned for local testing).
mvn -pl auth-service -am spring-boot:run
mvn -pl gateway-orchestrator-service -am spring-boot:run
mvn -pl payment-service -am spring-boot:run
mvn -pl api-gateway -am spring-boot:run
# …wallet, ledger, webhook, billing, payout as needed- Register / login:
POST http://localhost:8080/auth/v1/register,POST http://localhost:8080/auth/v1/login - Create payment:
POST http://localhost:8080/paymentswithAuthorization: Bearer <token>and body:
{
"amount": 10000,
"currency": "XOF",
"customerId": "00000000-0000-0000-0000-000000000001",
"provider": "AUTO",
"countryCode": "CI"
}- Webhook (dev, no secret):
POST http://localhost:8080/webhooks/flutterwavewith raw JSON body.
- Auth:
http://localhost:8081/auth/swagger-ui.html(direct) or via gateway where aggregated. - Each service exposes
/v3/api-docson its port.
Import postman/AfriPay.postman_collection.json and set baseUrl to http://localhost:8080.
mvn -pl auth-service -am package -DskipTests
docker build -f Dockerfile --build-arg MODULE=auth-service -t afripay/auth-service .mvn clean verify- Unit tests: JUnit 5 + Mockito (e.g.
LedgerPostingServiceTest). - Integration tests: add Testcontainers (Postgres/Kafka) per service as you harden flows.
- JSON-friendly logging: configure Logback JSON encoder in production (see Spring Boot docs); ship to ELK/OpenSearch.
- Prometheus (
http://localhost:9090) scrapes every service on/actuator/prometheuswith aservicelabel per target; config:infra/prometheus/prometheus.yml. - Grafana (
http://localhost:3000, defaultadmin/admin) ships with an AfriPay overview dashboard (HTTP rate, scrape health, JVM heap). Details: docs/MONITORING.md.
Resource servers use JWT today. To add OAuth2 Authorization Server or external IdP (Keycloak, Cognito), replace JwtDecoder beans with issuer URIs and register clients; gateway continues to validate JWTs the same way.
Proprietary — adjust for your organization.