McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. System Design Interview: Design a Payout System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

System Design Interview: Design a Payout System

A payout system design: 1) Payout Request Service (creates payout record with status "pending"), 2) Approval Service (dual approval for large amounts, auto-approve for small), 3) Transfer Dispatcher (calls POST /transfer when approved), 4) Status Poller (polls GET /transfer/:id or listens for transfer.success/failed webhooks), 5) Retry Handler (exponential backoff for failed transfers), 6) Audit Log (immutable record of every state change). Never transfer without first checking balance, never retry more than 3-5 times without human review.

In this article

  1. 1. Payout System Components
  2. 2. Idempotency for Money Movement
  3. 3. Failure Mode Discussion
  4. 4. Learn More

Payout System Components

Finance Team / Automated Trigger
          |
          | POST /payouts/request
          v
Payout Request Service
   - Creates payout record: { id, recipient_code, amount, status: 'pending', created_by, created_at }
   - Inserts into payout_requests table
          |
          | (amount > threshold → approval required)
          v
Approval Service
   - Sends notification to approver(s)
   - Approver 1 approves → status: 'pending_second_approval'
   - Approver 2 approves → status: 'approved'
   - Either rejects → status: 'rejected', notify requester
          |
          | (status = 'approved')
          v
Transfer Dispatcher
   - Checks Paystack balance (GET /balance)
   - Calls POST /transfer with idempotency key = payout_id
   - Updates status: 'transfer_initiated', stores transfer_code
   - Returns transfer_code to audit log
          |
          | (async)
          v
Status Resolver (webhook OR polling fallback)
   - transfer.success → status: 'completed'
   - transfer.failed → status: 'failed', trigger retry or escalate
   - No webhook in 24h → poll GET /transfer/:code

Idempotency for Money Movement

Never call POST /transfer twice for the same payout request. Use your payout_id as the idempotency key:

async function dispatchTransfer(payoutRequest) {
  // Check if we already initiated a transfer for this payout
  if (payoutRequest.transfer_code) {
    // Already dispatched — check status, do not re-dispatch
    return await getTransferStatus(payoutRequest.transfer_code);
  }

  var res = await fetch('https://api.paystack.co/transfer', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      source: 'balance',
      amount: payoutRequest.amount,
      recipient: payoutRequest.recipient_code,
      reason: payoutRequest.reason,
      reference: 'payout_' + payoutRequest.id, // unique per payout
    }),
  });

  var data = await res.json();
  // Store transfer_code before returning — if we crash here, the payout_request still has the code
  await db.payoutRequests.update(payoutRequest.id, { transfer_code: data.data.transfer_code, status: 'transfer_initiated' });
  return data.data;
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Failure Mode Discussion

  • Insufficient balance — check GET /balance before dispatching. Queue the transfer for when balance is sufficient, or alert finance team. Never retry blindly on insufficient balance errors.
  • Bank account invalid — transfer.failed with gateway_response "Invalid Account Number". Do not retry. Alert the requester to verify recipient details.
  • Network timeout during POST /transfer — you don't know if Paystack received the request. Check the payout_requests table: if transfer_code is null, you can safely retry (Paystack deduplicates on reference). If transfer_code is set, poll GET /transfer/:code for status.
  • transfer.success webhook never arrives — poll GET /transfer/:code after 24 hours. If status is "success", update DB. If still "pending", escalate to Paystack support.

Learn More

See system design: payment service for the payment collection version.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Payout requests must be idempotent — retrying a failed transfer must not create a second transfer.
  • ✓Dual approval workflow for large amounts is a security and compliance requirement, not optional.
  • ✓Every state change in a payout (pending → approved → sent → success/failed) must be audited.
  • ✓Check Paystack balance before dispatching transfers — insufficient balance returns a 400 error.
  • ✓Distinguish between "transfer not initiated yet" and "transfer initiated but no confirmation" — different recovery paths.

Frequently Asked Questions

What amount threshold should I suggest for dual approval?
In your interview answer, propose a configurable threshold rather than a hardcoded one. Common patterns: NGN 500,000 / KES 50,000 requires dual approval, larger amounts require CFO approval. The exact threshold is a business decision — what matters in the interview is that you know dual approval is required and that the threshold should be configurable.
Should the payout system use webhooks or polling to detect transfer status?
Both, with webhooks as primary. Configure Paystack to send transfer.success and transfer.failed webhooks. This handles the happy path quickly. For the fallback: a background job polls GET /transfer for any payouts in "transfer_initiated" status for more than 30 minutes with no webhook. This covers Paystack webhook delivery failures.
How do you handle bulk payouts (1,000+ vendor payouts) in the system?
Use the Paystack bulk transfer endpoint (POST /transfer/bulk). In your system design, the Transfer Dispatcher batches payout requests into groups of up to 100 (Paystack's batch limit), calls bulk transfer for each batch, and stores the batch_id. The Status Resolver polls GET /transfer/bulk/:batch_id for status updates. Approval workflow runs per individual payout before batching.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Getting Paid to Build Paystack Integrations

Payment integration is one of the most bankable skills a developer in Africa can carry. This guide covers how to price the work, land clients, ace fintech interviews, and build a career or agency around Paystack.

Read more

Landing Fintech Clients as a Kenyan Developer

Fintech client work in Kenya is available — but it does not come through random job boards. It comes through relationships, visibility in the right communities, and demonstrating that you know the Kenyan payment ecosystem.

Read more

From Frontend Developer to Payments Engineer

A practical transition guide for frontend developers who want to move into payment engineering. Covers the backend skills you need to learn, the payment-specific concepts that matter most, and how to build credibility in fintech.

Read more

Maintaining Client Payment Integrations as Recurring Revenue

Payment integrations break quietly. A webhook stops firing, a settlement account gets flagged, an API update changes a field name. Clients pay for peace of mind that someone is watching. This is your recurring revenue opportunity.

Read more

Build a Hotel Booking and Deposit System

A build guide for a hotel booking system that collects a deposit (typically 30-50%) via Paystack at booking, holds the balance due on checkout, and handles cancellations with partial refunds based on your cancellation policy.

Read more

Paystack vs Kora: Engineering Comparison

Kora (formerly Korapay) is a pan-African payment infrastructure provider focused on marketplace split payments, card acquiring, and business payouts. It overlaps with Paystack on standard checkout but differentiates on marketplace architecture and high-volume card acquiring economics.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.