WorkFinancial & marketplace infrastructure
Amana Market
The platform behind CoAmana’s market hubs: member onboarding and KYC, buy-now-pay-later ordering, wallets and payout rails, finance automation and analytics, built as an event-driven Laravel monolith where every money-moving step runs off the request thread.
Stack as evidence
Laravel · MySQL · Redis · Horizon · Octane · Docker · S3
Concerns
BNPL credit · Wallets & ledgers · Payment rails · Idempotent webhooks · Finance automation · Observability
- 01
Order
BNPL or cash, created via API
- 02
Events
Fees, commissions, notifications
- 03
Redis queues
Horizon supervisors: orders · verifications · products
- 04
Wallets & ledger
Transactions, not balance edits
- 05
Payment rails
Bank, card, mobile money · webhooks queued
- 06
Repayment & credit
Streaks, limits, reminders
- 07
Finance & reporting
Exports to S3, advice mails queued
01The problem
A marketplace is a financial system wearing a product
Amana Market digitises physical market hubs: members are onboarded and verified, merchants raise buy-now-pay-later or cash orders, and the platform moves money between buyers, sellers, market wallets and partner banks. Accepting a single order debits and credits wallets, triggers a bank transfer, schedules repayments and adjusts credit exposure.
Done synchronously, that chain would hold a database lock while waiting on a payment rail, and a buyer closing their browser could leave a payout half-finished. The engineering problem was making every one of those steps complete, exactly once, regardless of what the request, the provider or the worker did next.
02The constraints
What could not simply be changed.
Money cannot be approximately right
Analytics can lag. Wallet balances, repayments and payouts cannot. Every design started from what happens when a callback arrives twice, or never.
Providers set the terms
Bank, card, mobile-money and POS partners each have their own webhooks, signatures, latency and failure modes. The system had to be correct across all of them.
Finance works in spreadsheets
Reconciliation and bulk order closure arrive as uploaded sheets. The platform had to ingest them safely rather than ask finance to change how they work.
A small team on call
Monitoring had to be cheap, homegrown and loud enough that operations hear about a failing rail before customers do.
03The architecture
The application is a Laravel monolith served through Octane on FrankenPHP, with domain services behind REST and Livewire controllers. Anything that moves money, calls a partner or takes more than a moment is expressed as an event whose listeners dispatch jobs onto Redis. Horizon runs dedicated, auto-scaling supervisors for orders, verifications and products with hour-long timeouts, so a spike in payouts or KYC never starves catalog updates or vice versa.
Order acceptance is the core pipeline. A queued listener runs inside a database transaction: it credits and debits the relevant wallets, requests the partner bank transfer, creates the repayment schedule and hands off to credit-limit adjustment. Wallets are split by purpose, with user, market, insurance and payout ledgers plus a partner-bank transactions table, so any payout can be traced from order to ledger entry to the exact bank payload.
Inbound money follows the same discipline in reverse. Bank, card and POS webhooks are signature-verified, pushed onto queues before any wallet is touched, and processed inside transactions so retries are idempotent. BNPL lives in its own tables for upcoming payments, repayments and a credit queue; repayments update streaks, streaks feed automated credit-limit changes, and reminders go out over queued SMS.
Finance automation reuses the queue as a backpressure valve. Large spreadsheet uploads that close orders in bulk are chunked into delayed, queued closures so they never contend for locks with live orders. Exports render to S3 in chained jobs that then email the requester, and every mailable in the system is queued. Reliability is watched with deliberately simple tools: alerts when database connections approach their limit, structured exception reporting to Slack, a payment-rail health check that records outage streaks and auto-resolves, a twice-daily SMS balance check, Redis counters rolled into product metrics, and a random ten-percent audit of recorded calls each week.
04Engineering decisions
01
Make order acceptance an asynchronous pipeline
The alternative was a nightly reconciliation cron or a long synchronous request. Neither survives a slow payment rail. A transactional, queued listener keeps HTTP fast and guarantees payouts complete even if the requester disconnects.
02
Queue webhooks before touching wallets
Partner traffic arrives on the partner’s schedule. Accepting, verifying and enqueueing first means a bank API failure becomes a retry rather than a lost payout.
03
Ledgers per wallet type, not one balance column
Separate user, market, insurance and payout ledgers with a partner-bank transactions table cost more tables and metadata. In exchange every unit of money is traceable and replayable.
04
Dedicated Horizon supervisors per workload
Orders, verifications and products have different latency profiles. Isolating them with their own auto-scaling and timeouts stops one backlog becoming everyone’s backlog.
05
Layer BNPL onto the existing order model
Working-capital flags, repayment streaks and a credit queue were added on top of the classic order tables rather than in a new system, so cash and credit orders share one settlement path.
06
Homegrown health gates over heavy tooling
Service logs, a rail health command, balance checks and Slack alerts are unglamorous, but they are cheap to run and the team understands every line of them.
05The trade-offs
What complexity I introduced, on purpose.
- The order-acceptance listener carries many branches and is harder to unit-test than a simple handler. The atomic guarantees were worth it.
- More tables and more metadata than a single balance column. Every payout is traceable, at the cost of more schema to understand.
- Queue discipline is mandatory. When everything important is asynchronous, monitoring queue depth stops being optional.
- Homegrown observability needs people who read it. It keeps dependencies minimal but relies on operational habit.
06The outcome
- Payouts, wallet movements and repayment schedules complete even when the original request disconnects or a provider fails mid-flight.
- Every payout is traceable from order to ledger entry to bank payload, and every partner event is replayable.
- Finance reconciles, exports and notifies suppliers asynchronously, so their load no longer touches customers.
- Operations receive Slack, email and SMS warnings about failing rails and exhausted balances before customers notice.
- The platform evolved from cash marketplace to BNPL credit without a rewrite of the order model.
07The lesson
In financial systems the interesting design work is almost entirely about failure. The happy path is the easy part; the architecture is what happens when the callback arrives twice.