Skip to main content

SDLC 02: System Design and Architectural Decisions

Revision history: Updated May 2026 — AWS EC2 hosting; PawaPay + Flutterwave payment architecture; microservices added; DPO removed.


1. High-Level Architecture

[End Users / Browsers / Mobile Apps]
|

Cloudflare (DNS · WAF · CDN · SSL)
|
┌────┴────────────────────────────────────┐
│ │
▼ ▼
Next.js Frontend (Vercel) AWS EC2 — Express API
│ │
│ API calls (/api/v1/*) │
└──────────────────────────────────┘

┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
PostgreSQL (Prisma) Redis Cache Cloudinary (files)

│ payment events
┌────────┴──────────┐
│ │
▼ ▼
PawaPay Flutterwave
(MoMo primary) (Card + MoMo failover)


Middleware.io (APM)

2. Microservices Architecture

The Pakashop backend is composed of one monolithic Express API and several supporting microservices, all hosted on AWS EC2:

ServiceResponsibility
express-apiCore business logic: auth, orders, products, shops, payments
notification-serviceIn-app + email notification fanout
moderationSightengine content moderation for uploaded images
config-serviceCentralised feature flags and configuration
api-gatewayNginx reverse proxy; routes external traffic to services

3. Key Architectural Decisions

3.1 Hosting: AWS EC2 (replacing Vercel for backend)

  • Decision: Express API and microservices deployed on AWS EC2 instances.
  • Rationale: EC2 provides persistent WebSocket connections, long-running background jobs (polling, settlement), and direct Redis access — requirements not met by Vercel's serverless edge runtime.
  • Frontend remains on Vercel for CDN and preview deployment benefits.

3.2 Database: Prisma ORM + PostgreSQL on EC2/RDS

  • Decision: Prisma as the sole ORM for PostgreSQL.
  • Rationale: Type-safe migrations, strong typing, and seamless Node.js integration.
  • Connection pooling: PgBouncer or Prisma Accelerate recommended for high-concurrency endpoints.

3.3 Caching: Redis

  • Decision: Redis deployed on EC2 (same VPC as Express API).
  • Use cases: User sessions, cart data, rate-limiting buckets, temporary payment status, product listing cache.
  • See ../caching-strategy.md for key naming and TTL conventions.

3.4 Payment Architecture: PawaPay + Flutterwave (replacing DPO)

  • Decision: PawaPay as the primary mobile money provider; Flutterwave for card payments and mobile money failover.
  • Rationale: PawaPay is purpose-built for Zambian MNOs (MTN, Airtel, Zamtel); Flutterwave provides SAQ-A-compliant card processing.
  • PCI-DSS: Card data never touches Pakashop servers; all card entry handled by Flutterwave's hosted payment page.
  • See ../payment-architecture.md for the full implementation.

3.5 Content Moderation: Sightengine

  • Decision: All image uploads run through the moderation microservice which calls Sightengine.
  • Rationale: Automated NSFW/violence detection before images appear on the marketplace.
  • See ../content-moderation.md.

3.6 Observability: Middleware.io

  • Decision: OpenTelemetry-based APM via Middleware.io across all services.
  • Rationale: Unified traces, logs, and metrics from a single dashboard without managing a self-hosted Grafana stack.

3.7 Email Infrastructure Split

  • Resend: Transactional emails (OTPs, order confirmations, password reset). High deliverability, analytics.
  • Zoho Mail: Human-to-human customer support correspondence.

3.8 File Uploads: Cloudinary

  • All product images, shop logos, and KYC documents upload to Cloudinary with signed authentication.
  • After upload, the URL is stored in PostgreSQL; Sightengine moderation runs asynchronously.

3.9 DNS and Security: Cloudflare

  • Full DNS management, WAF rules (OWASP top 10), and CDN via Cloudflare.
  • GoDaddy acts only as domain registrar with NS records pointing to Cloudflare.

4. Data Flow Examples

4.1 Mobile Money Checkout

1. Customer selects MTN Money on /checkout
2. Frontend → POST /api/v1/orders → Order created (PENDING)
3. Frontend → POST /api/v1/payments/initiate
4. PaymentOrchestrator → PawaPayAdapter.initiateDeposit()
5. PawaPay sends USSD push to customer's phone
6. Backend returns { type: 'ussd', transactionId, message }
7. Frontend shows MobileMoneyOverlay, polls GET /api/v1/payments/status/:orderId every 5 s
8. PawaPay webhook → POST /api/v1/payments/webhook/pawapay
9. WebhookHandler verifies HMAC → marks payment CAPTURED → Order = PAID/CONFIRMED
10. Poll detects PAID → redirect to /orders/:id

4.2 Card Checkout

1. Customer selects Visa/Mastercard on /checkout
2. Frontend → POST /api/v1/orders → Order created (PENDING)
3. Frontend → POST /api/v1/payments/initiate
4. FlutterwaveAdapter generates hosted payment link
5. Frontend redirects to Flutterwave's hosted page
6. Customer enters card details on Flutterwave (raw card data never reaches Pakashop)
7. Flutterwave redirects to /payment/callback?orderId=...
8. Flutterwave also POSTs to /api/v1/payments/webhook/flutterwave
9. Callback page polls status → shows success

4.3 Product Image Upload

1. Merchant uploads image → Express API requests Cloudinary signed signature
2. Frontend uploads directly to Cloudinary using signature
3. Cloudinary URL stored in PostgreSQL via Prisma
4. Background job → moderation service → Sightengine API
5. If violation: image flagged, product hidden until manual review

5. Environment Configuration

VariablePurposeScope
DATABASE_URLPostgreSQL connection stringAll
REDIS_URLRedis connection URLBackend
RESEND_API_KEYTransactional emailBackend
CLOUDINARY_CLOUD_NAMECloudinary configBackend
CLOUDINARY_API_SECRETSigned upload authBackend
PAWAPAY_API_KEYPawaPay deposits & payoutsBackend
PAWAPAY_WEBHOOK_SECRETHMAC signature verificationBackend
FLUTTERWAVE_SECRET_KEYFlutterwave API authBackend
FLUTTERWAVE_SECRET_HASHWebhook verif-hashBackend
MIDDLEWARE_API_KEYMiddleware.io APMAll
SIGHTENGINE_API_USERContent moderationservice
SIGHTENGINE_API_SECRETContent moderationservice
JWT_SECRETSession token signingBackend
PAYMENT_PROVIDERMOCK | AUTO | PAWAPAYBackend