Back to portfolio
Case StudyJatri · Counterman POS

Rebuilding counter ticketing for Bangladesh's largest travel platform

Led frontend and API contract design for Jatri's counterman ticketing module — an offline-first POS powering 50+ counters across 15 bus and launch operators, handling 30k+ tickets daily at Eid peak.

Vue 3NuxtNestJSMongoDBWebSocketRedisIndexedDBESC/POS

Role

Lead Frontend + API design

Team

30 engineers

Timeline

6 months to launch

Status

In production, nationwide

Context

Jatri operates Bangladesh's largest multi-modal travel platform. Before this module, 15 bus and launch operators ran ticketing on fragmented legacy systems — separate seat maps, manual reconciliation, no cross-counter visibility.

Countermen at 50+ physical counters booked tickets the old way: paper, phone calls, stuck on local LAN. We rebuilt it from scratch as a unified counterman POS — offline-first, with live seat sync across the country.

Constraints
  • Peak load: Eid rush — 15 operators × ~2,000 tickets/day = 30k+ tickets/day, concentrated in hours.
  • Operator resistance: Countermen trained for years on legacy flow. New system had to match muscle memory or adoption fails.
  • Network reality: Rural counters, flaky internet. Launch terminals on remote connections.
  • Hardware: Thermal printers with operator-specific challan layouts — every operator demanded their own legal format.
  • Throughput per counterman: avg 70 tickets/shift, peak 200 during Eid rush.
Hard problems solved

Same seat, two PNRs — by design

Launch (ferry) tickets have a quirk: a single physical seat on a long-haul route can legally be sold twice when passengers board and alight at different points (Passenger A: Dhaka → Barisal, Passenger B: Barisal → Chittagong — same seat, same boat, two PNRs).

No competitor supported it. Legacy systems blocked it as a "double booking." We modeled seats as segmented inventory — each seat is a graph of boarding → alighting edges, availability computed per-segment, not per-seat. A booking consumes only edges it traverses.

Frontend renders the same seat as bookable or blocked contextually based on the counterman's selected origin–destination pair.

Race conditions at scale

Two countermen clicking the same seat simultaneously during Eid rush was routine. Initial approach (API availability check → book) produced double-bookings in prod.

  • Pessimistic DB lock on seat-segment rows during commit transaction — guarantees correctness.
  • WebSocket broadcast of seat state changes to all connected counters — cuts race window to milliseconds, gives real-time UX (seat turns red on every screen the instant someone grabs it).
  • 20-min hold TTL on in-progress carts, auto-released via background job.

Offline-first for launch counters

Ferry terminals lose connectivity for hours. Tickets still have to print. Built:

  • Local IndexedDB cache of seat inventory + fare rules, synced on connection.
  • Optimistic booking queue — counterman prints challan offline, transaction replays on reconnect.
  • Server-side reconciler detects offline overlap, surfaces to ops for manual resolution (<0.1% of cases).

Bulk booking UX

A counterman routinely books 10–200 seats per transaction — family groups, corporate charters, Eid migrations. UX had to let them:

  • Select non-contiguous seats across multiple rows with keyboard + click.
  • Assign different passenger info per seat (name, phone, age, gender — gender-segregation rules enforced).
  • Mix boarding / alighting points across tickets within same transaction.
  • Split payment: cash + bKash + card on one invoice.
  • Commit atomically — all seats book or none do.

Built as a multi-step cart with live fare recomputation via WebSocket-bound state.

Per-operator fare + commission engine

Every operator has its own pricing: peak multipliers, student discount, senior citizen rules, seat-class premiums, route surcharges, counterman commission tiers, platform commission. Legacy was hardcoded. Modeled as a rules engine — operators configure fare matrices via admin panel, engine computes at booking time. Frontend shows live breakdown so countermen can explain to passengers on the spot.

Challan printer integration

Each operator's legal challan had its own template — column order, logo placement, tax ID positioning, regulator-mandated fields (BRTA / BIWTA). Built a template renderer that compiles ESC/POS commands per operator and streams to thermal printers over USB / network, with print retries and jam handling.

Refund / cancellation flow

Partial refunds on segmented seats — refund one leg of a multi-segment booking without disturbing the other. Cascading rules: time-to-departure tiers, operator policy, payment-method rollback (wallet instant, card via PG, cash via counter).

Adoption strategy

Operators refused new UI initially — retraining 50 counters is expensive. We improvised:

  • Kept legacy keyboard shortcuts (Ctrl+N new ticket, F2 print, etc.) so muscle memory transferred.
  • Shipped a "trainer mode" overlay for first two weeks.
  • On-ground rollout team shadowed counters during first Eid rush.

Adoption hit 100% by month 3.

Results

30k+

Tickets / day at peak

50+

Counters nationwide

15

Operators onboarded

0

Double-bookings post-launch

~35s

Avg booking time (90s → 35s)

6 mo

Time to ship

  • First platform in Bangladesh with segmented-seat launch ticketing + offline sync.
  • Powers ticket issuance for 250M+ lifetime tickets across 15 operators.
  • Rolled out across Bangladesh, Saudi Arabia, and the UAE.
What I'd do differently
  • CRDTs for offline sync instead of server-reconciler queue — would remove the <0.1% manual cases.
  • Split seat-segment engine into its own service earlier — it became the bottleneck service and should've been isolated from day 1.