Adeel Nayyer
← All work

Payments / Platform · client work, details anonymised

Ledgerline

The payment service behind a hospitality marketing platform. It onboards merchants and takes card payments for gift cards, event tickets, contract services and online orders, and it also bills the platform's own businesses for their subscriptions.

Role
Lead backend engineer
When
Jul 2025 – Oct 2026
Source
Client code, not public
Stack
  • TypeScript
  • Express
  • Prisma
  • Aurora PostgreSQL
  • Worldpay for Platforms (Payrix)
  • AWS Lambda + SQS FIFO
  • App Runner
  • Terraform
  • 1,680 of the repository's 1,692 commits, from the first one in July 2025 to October 2026, with 15 ADRs recording the larger decisions
  • Every webhook that passes its signature check is saved before it is queued, so an event can't vanish between Lambdas, and payment status is worked out from those events
  • Merchant Subscriptions with dunning that Pay owns: recovery retries inside the grace period, one card change charge for what's outstanding, and a failure count kept separate from the provider's
  • 167 test files across unit, integration and live sandbox suites, plus Stryker mutation testing run module by module
Ledgerline: commands through one seam, status rebuilt from webhook eventsCALLERSAPI · APP RUNNERPROVIDER & WEBHOOKSDATA & STATUSPlatform servicesgift cards, tickets, ordersHosted card fieldsbrowser, token onlyExpress APIidempotent, signed callsProvider seam~7 verbs + in-memory fakeStatus reducerfurthest state winsPayrixWorldpay for PlatformsEvent capture Lambdasignature checkProcessor LambdaDLQ, failures by causeAurora PostgreSQLevents, paymentsCloudWatchalarms by outcomeauthorise, refundcard tokencharges, onboardingalertsSQS FIFOsave firstcorrelatereduce events
Ledgerline: commands through one seam, status rebuilt from webhook events

Problem

The platform sells several things on behalf of the restaurants and venues it serves: gift cards, event tickets, contract services and online orders, some of them delivered by a courier. Each one needs card capture, refunds and payouts to the merchant, and each merchant has to be onboarded with the payment provider first. Separately, the platform charges those same businesses a monthly fee for its own products, so the service also had to run recurring billing in the opposite direction. All of this had to live in one service that other parts of the platform could call.

My role

I was the lead backend engineer and wrote almost all of the code: 1,680 of the 1,692 commits between July 2025 and October 2026. That covered the API, the data model (87 Prisma models over 116 migrations), the webhook pipeline, the Terraform for AWS, and the test suites. I also wrote the ADRs and the domain glossary that later work was checked against.

Architecture

An Express API in TypeScript runs on App Runner, with Prisma on Aurora PostgreSQL. Card details never reach it. Cards are tokenised in the browser with the provider’s hosted fields, so the service only ever handles tokens, which keeps it out of the heaviest PCI scope. Money-moving endpoints require an idempotency key, and service-to-service calls are signed.

Webhooks come in through API Gateway to a capture Lambda that checks the signature, saves the event and puts it on an SQS FIFO queue. A second Lambda processes it, and events that keep failing land in a dead-letter queue whose processor sorts the failures by cause. Courier deliveries go through Uber Direct with their own webhook lifecycle, and its CloudWatch alarms are split by outcome, so a burst of bad signatures raises a different alarm from a run of processing errors. A billing job handles Merchant Subscription retries and outstanding charges.

Key decisions

  • Status from events, not from columns. A payment’s lifecycle state is derived from its saved webhook events. The reducer takes the furthest state reached (created, authorised, captured, settled) whatever order events arrive in, and lets reversals such as voids, returns and chargebacks override. A dropped or late webhook can no longer move a settled charge backwards.
  • Read the provider’s real payloads. Real production alerts turned out to carry no event name at all, so every one was being classified as unknown and dropped. The fix derives the event from the alert subject, cross-checked against the numeric status, and the tests now run on captured payloads instead of invented ones.
  • One provider behind a narrow seam. Finix was spiked as a second provider and then retired. An earlier ADR had split the provider interface into five ports on the assumption that two providers would run at once. I replaced it with about seven provider-neutral verbs, tested against an in-memory fake so the seam has a second implementation.
  • Pay owns dunning. Payrix only charges each subscription cycle. Retries, grace periods and the decision to cancel stay with the platform, and Payrix’s own failure limit is set high enough that it never cancels a subscription first.
  • Fewer, generic endpoints. The API had grown to about 270 product-specific routes. The recorded direction is a dozen offer-agnostic primitives (authorise, capture, refund, void) with opaque metadata, and a single Payment aggregate backed by an append-only event log. These two ADRs are accepted but not yet built.

Outcomes

The service runs merchant onboarding, card payments, partial refunds, payouts and courier deliveries for the platform’s merchants, along with subscription billing for the platform itself. The Lambdas, queues and dead-letter handling are all in Terraform, along with the alarms and dashboards. The test suite has 167 files, from unit tests to live runs against the provider’s sandbox, and mutation testing runs module by module so weak tests show up as surviving mutants.