Engineering deep diveHow one person runs 100+ orders a day
For engineers: the order lifecycle, how a pharmacy is found, and how the system stays safe when messages and deliveries fail.
Stack
- API
- Node.jsExpressJoi
- Jobs
- BullMQRedisnode-cron
- Data
- MongoDBMongoosegeo index
- Integrations
- WhatsApp APILalamoveGoogle Maps
- Admin
- ReactTypeScriptViteTailwind
- Quality
- JestDockerNginx
Four queues, one job each
- orderQueueTakes new orders from the online store, checks the address with Google Maps and starts the flow. Retried safely.
- pharmacyQueueFinds nearby pharmacies and sends each one a WhatsApp order request.
- chatbotQueueHandles WhatsApp replies from customers and pharmacies, such as confirm, accept or decline.
- deliveryQueueBooks the courier and processes Lalamove status webhooks.
One order, from checkout to doorstep
Every step is driven by the order's status, so the next action is always clear.
- StoreA new order arrives by webhook and is queued. The store order ID makes a repeat harmless.
- CustomerThe customer confirms the order on WhatsApp. No reply in time triggers a timeout path.
- MatchingNearby partner pharmacies are found with a MongoDB geo query and asked on WhatsApp.
- FallbackIf no partner can take it, the search widens to any pharmacy. Those are paid upfront instead of on credit.
- AcceptThe first pharmacy to accept locks the order. The others are told it is no longer available.
- DeliveryA Lalamove quote is fetched. Staff pick Lalamove or manual delivery, and the courier is booked.
- TrackingLalamove webhooks map to order states and send the customer WhatsApp updates.
Order states
Pending→Customer confirmed→Awaiting pharmacy→Pharmacy confirmed→Booking delivery→Out for delivery→Delivered/Manual handling
More than 20 states in total. Exceptions go to a manual queue instead of getting stuck.
Key decisions
WhatsApp as the pharmacy app
- Problem
- Pharmacies would not install a new app just for us, and phone calls did not scale.
- Choice
- Every request, reply and update goes through approved WhatsApp templates with buttons. A reply comes back as a webhook and moves the order forward.
- Why
- Zero onboarding for pharmacies, and every answer is recorded against the order.
Partner first, then widen
- Problem
- Partner pharmacies are cheaper and work on credit, but they cannot cover every area.
- Choice
- Ask nearby partners first. If none accepts, ask any nearby pharmacy, with an upfront payment step for non-partners.
- Why
- Most orders go to the cheaper partner, and no order is lost because an area has no partner.
Retry only what is safe to repeat
- Problem
- A short database or Google outage used to drop a new order forever.
- Choice
- New-order jobs retry 3 times with backoff, because they dedupe on the store order ID. WhatsApp and courier jobs do not retry, because a repeat would send twice or book twice.
- Why
- No lost orders, and no double messages or double bookings.
Accepting an order, simplified
pharmacy.orchestrator.jssimplified sketch
async handlePharmacyResponse(orderId, pharmacyId, isAccept) {
const order = await OrderService.getOrderById(orderId);
// only orders still waiting for a pharmacy can be taken
if (!AWAITING_PHARMACY.includes(order.status)) throw new AppError('Order closed', 400);
if (order.pharmacyId) throw new AppError('Taken by another pharmacy', 409);
if (!isAccept) return recordDecline(order, pharmacyId);
const pharmacy = await PharmacyService.getPharmacyById(pharmacyId);
const quote = await fetchLalamoveQuotation(order, pharmacy); // never throws
await OrderService.updateById(orderId, {
pharmacyId,
lalamoveQuotation: quote,
// partners work on credit, others pay first
status: pharmacy.creditAllowed ? AWAITING_DELIVERY_METHOD_CHOICE : NON_PARTNER_PHARMACY_CONFIRMED,
});
await notifyOtherPharmacies(orderId, '003_partner_store_order_expired');
}
Scale
100+orders a day, up from about 20
5x+capacity in the first month
4background queues
20+order states
Reliability and operations
- Timeouts by cronScheduled jobs expire orders and handle customers who never confirm, with a guard so a job never runs twice at once.
- Audit logEvery status change and error is written to an audit log tied to the order.
- Tested coreJest with an in-memory MongoDB covers the order, pharmacy and delivery flows.
- Strict layersRoute, validation, controller, orchestrator, service, model. Services never call each other directly.