Md. Nayeem Ahmed
All projects

Courier backend

A parcel delivery backend with cash on delivery and merchant payouts, modelled on couriers like Steadfast and Pathao.

My role
Sole developer. Planned and built as an 8-week MVP in 35 pull requests.
Stack
Java 21, Spring Boot, Spring MVC, JPA, PostgreSQL, Flyway
Status
MVP features are complete. Final testing is in progress before deployment.
Links
Backend code
Screenshot coming soon

The problem

A courier service is more than storing records. At any moment it must know who has each parcel, what state it is in, which hub it is at, and how much money is owed to whom.

A parcel goes from the merchant to a pickup rider, through a hub, to a delivery rider who collects the cash, and finally the merchant is paid after fees. Any step can fail or be retried, and the money must always add up.

Who it is for

It is designed for three groups: merchants who send parcels, riders who pick up and deliver them, and admins who manage hubs and approve payouts. It has not been deployed yet, so it has no users.

What I built

  • Merchant registration, hubs, riders and user management.
  • Delivery pricing, parcel creation and pickup requests.
  • A parcel status workflow with full history and public tracking.
  • Pickup and delivery flows for riders.
  • A cash-on-delivery ledger, a merchant dashboard, and payout requests with admin approval.

Architecture

The project is split into Maven modules by business area: users, hubs, pricing, parcels and the ledger. Each module owns its own tables, so the parcel module cannot change ledger data directly. I chose Spring MVC so that a status change, its history and its ledger entry are saved in one transaction.

riderparcelhistorydelivered eventledger
The rider marks the parcel as delivered. Only the rider assigned to that parcel can update it.

Key decisions

Optimistic locking on parcels

Each parcel has a version number. If two people update the same parcel at the same time, the second update fails instead of silently overwriting the first.

Database migrations from day one

I used Flyway instead of letting Hibernate generate the schema. That made it possible to store money with exact precision, enforce unique tracking numbers and seed pricing rules, and it keeps development and production databases identical.

Net payouts

Merchants are paid the cash collected minus delivery and cash-handling fees. Any negative balance carries over to the next payout, so the ledger always balances.

How I solved it

Payouts are where money bugs hide. A merchant might request a payout twice, or two admins might approve and reject the same payout at once. When a payout is requested, the merchant’s open ledger entries are locked, so a second request has to wait and then finds nothing left to claim:

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select e from CodEntry e where e.merchantId = :merchantId and e.settlementId is null "
        + "and e.status <> com.delivery.ledger.enums.CodEntryStatus.PENDING")
List<CodEntry> findSettleableForUpdate(@Param("merchantId") Long merchantId);

Approving or rejecting a payout locks it in the same way, so two admins are handled one after the other. Both cases are covered by tests. I also tested the complete flow from merchant sign-up to payout and logged every issue I found to fix before deployment.

What’s next

  • Fix the remaining issues from end-to-end testing, then deploy.
  • Make parcel creation safe to retry, so a repeated request cannot create two parcels.
  • Send status updates to merchants and receivers, then add proof of delivery.