Talha Riaz
← All work
Zero to production, solo

HoldingHands

When the product is trust, the ledger is the product.

Team4Tech Solutions · 2021 to 2023

8 weekszero to productionAs the sole technical hire, money handling included
~40%faster dashboard readsCache aside on aggregates plus indexes chosen from real query profiles

HoldingHands is a community mutual aid platform. Members contribute a recurring monthly amount into a shared pool, and the pool funds admin authorized disbursements to members facing health, school or urgent needs.

There is no product market fit ambiguity to design around here. The product is trust infrastructure. Every dollar in and out has to be traceable, every disbursement has to be authorized by an accountable person, and none of that could be cut short by the fact that one engineer had eight weeks.

The formula that determines each member's monthly contribution belongs to the organization and is not described here. The architecture treats it as an admin configured, versioned input rather than something the system decides.

What I owned

Founding engineer and sole technical hire. Owned the whole product: schema design, the Node and Express API, the React frontend, the data layer across PostgreSQL, MongoDB and Redis, all Stripe money movement, and the CI/CD and review practices the team kept using after it grew past one engineer.

Scale

Zero to production in 8 weeks as the only engineer, with every dollar in and out of a community pool traceable.

A pool balance you cannot reconstruct is a number you cannot defend

The obvious way to track a shared pool is a balance column you add to and subtract from. It is also the way you end up unable to answer the only question that matters when someone disputes a figure, which is why the number is what it is.

So the ledger is append only, and the pool balance is a derived sum rather than a stored counter. Every contribution marked paid and every disbursement marked paid writes exactly one entry. The balance can always be rebuilt from history and audited against Stripe.

Money in and money out are deliberately not symmetric. Money leaving the system is higher risk than money entering it, so it was built and tested last, with more scrutiny, and a disbursement above a configured threshold requires a second admin's approval before the transfer fires. Encoding that as a status transition guard rather than a policy document is what actually stops it being skipped when someone is in a hurry.

The polyglot storage was a fit decision, not indecision. Money needs ACID guarantees, so it lives in Postgres. Case requests genuinely differ by category, a medical case needs different fields from a tuition request, and the category set was expected to grow, so they live in MongoDB rather than a wide sparse table or an EAV pattern.

What it already handles

  • Queue unavailable

    Redis unreachable at enqueue time

    ENQUEUE
    QUEUE X
    PENDING JOB
    Work delayed, never lost
  • Large disbursement

    Amount over the configured threshold

    ADMIN 1
    THRESHOLD
    ADMIN 2
    Maker checker enforced in code, not policy
  • Simultaneous approvals

    Two admins act on one case at once

    BOTH
    LOCK
    ONE PAYS
    No double disbursement, structurally
  • Approval revoked late

    Admin reverses just before the transfer fires

    REVOKED
    RECHECK
    HELD
    The revocation wins the race
  • Contribution refunded after disbursement

    Member disputes money the pool already paid out

    REFUND
    OFFSET
    ADMIN ALERT
    Never an automatic and wrong clawback

Failure modes

  • Failure

    Stripe webhook spoofed or tampered with

    How it is caught

    Signature verification on every inbound webhook

    What happens next

    Rejected and alerted on. A webhook is never trusted on the shape of its payload alone, because a fake payment succeeded event is exactly the attack.

  • Failure

    Webhook delivered twice

    How it is caught

    Stripe event id already processed

    What happens next

    Unique constraint makes the duplicate a no op.

  • Failure

    Webhook never arrives

    How it is caught

    Nightly reconciliation against Stripe's own records

    What happens next

    Any drift raises an alert and is corrected with a compensating ledger entry rather than quietly ignored.

  • Failure

    Contribution charge fails

    How it is caught

    Stripe decline webhook

    What happens next

    Grace period and retry schedule, with the member flagged at risk and visible to admins rather than silently dropped. A mutual aid model depends on knowing who is actually contributing.

  • Failure

    Disbursement transfer fails on bad bank details

    How it is caught

    Stripe transfer.failed webhook

    What happens next

    Held in a distinct failed state rather than sitting in pending review. An approved but undelivered disbursement is surfaced explicitly instead of disappearing from the queue.

  • Failure

    Two admins approve the same case at once

    How it is caught

    Optimistic locking on the disbursement status

    What happens next

    The second approval fails the status transition check, which structurally prevents a double disbursement rather than warning about it in the UI.

  • Failure

    Job queue unreachable

    How it is caught

    Enqueue retries exhausted

    What happens next

    Falls back to writing a pending job row in Postgres, which a recovery sweep picks up once the queue is healthy. A queue being briefly down should delay work, not lose it.

What it is held to

  • Ledger to Stripe reconciliation drift

    Zero tolerance, checked nightly

    The one number that can never silently drift in a trust based fund.

  • Automatic contribution collection success

    ~99.5%, remainder via dunning

    Realistic given card decline base rates. The dunning path is what makes the overall rate acceptable, not first attempt success.

  • Urgent case review

    Decision within 48h

    Urgent requests are explicitly prioritized in the review queue. Health and school follow a tracked but non SLA cadence.

  • Dashboard endpoints, p95

    Sub second after caching

    Directly tied to the cache aside and indexing work.

Why I chose what I chose

  • Why two databases?

    Money movement needs ACID transactions and relational integrity, so contributions, the ledger and disbursements live in Postgres. Case requests genuinely differ by category and the category set was expected to grow, which relationally means either a wide sparse table or an EAV pattern, both of which are worse than the problem. Document storage fits the shape the data actually has. That is polyglot persistence as a fit decision, not schema indecision.

  • Why is the pool balance derived rather than stored?

    A stored counter can drift from the records underneath it, and the moment it does you cannot tell which one is wrong. Deriving it from an append only ledger means the balance is always reconstructable and always auditable against Stripe.

  • Why Express rather than NestJS, given I use NestJS elsewhere?

    At eight weeks solo, Express's lower ceremony was the right trade for time to first working payment flow. There was no DI container or module scaffolding to stand up before writing a real endpoint. The cost is less enforced structure, and I paid that down with review discipline and CI rather than with a framework.

  • Why lean on Stripe this heavily?

    As the sole engineer responsible for all money movement, minimizing the custom payment handling surface was itself the risk reduction. Stripe's webhook and idempotency primitives are well tested infrastructure that one engineer building in eight weeks should use rather than reimplement.

  • Why is this the simplest infrastructure in my portfolio?

    A non profit with one engineer and a tight budget is exactly the case where not reaching for Kubernetes or microservices is the senior decision rather than the absence of one. One well structured Express service on Fargate, one deploy unit, one thing to operate. The right infrastructure is a function of the actual constraint, and it looks nothing like what CoinPerps needs.

What I would reconsider

  • Several parts were built synchronously first, including contribution collection before it moved behind the job queue. Reasonable eight week sequencing, but retry and backoff semantics are much easier to get right before the first production failure than to retrofit after, so the queue version is what should exist from day one on a rebuild.
  • Nightly rather than real time reconciliation means drift can persist up to a day in the failure case. Acceptable at launch volume, and the first thing I would move to a streaming reconciliation as transaction volume grows, since detection latency on financial drift is itself a risk surface.
  • As the only engineer owning all the money movement code, there was no second reviewer on payment critical changes for the first stretch. Mitigated with deploy time smoke tests against Stripe test mode, but it was a real gap that only closed when the team grew.
Stack

Node.js · Express · React · PostgreSQL · MongoDB · Redis · BullMQ · Stripe · ECS Fargate · CI/CD