Imtiaz Mashrafee

Home / Work / Ride-pooling lifecycle with guarded transitions

Ride-pooling lifecycle with guarded transitions

A full-stack ride-pooling MVP for a fixed Tesla fleet in Dhaka, with a guarded request lifecycle, shared pools formed by route compatibility and an audit event for every transition.

Solo project, backend. TypeScript, Next.js, Express, PostgreSQL.

  • My roleSole contributor in the commit history. All 125 commits and 15 merged pull requests, with iterative work on the core ride service (26 September to 1 October 2026).
  • EvidenceChecked by me 71 written test cases exist in the repository. They were counted, not run for this page.

Playable system flow

Watch an accept meet the capacity guard

Pick a scenario and press Play. The sequential steps are recorded from the project test run. The concurrent case is an observation under a database mock.

The controls need JavaScript. The whole flow is written out below, step by step.

The flow in text

Request B fits A sequential accept, recorded.

  1. Request: Request B asks for 2 seats Recorded project run

    What came in
    A passenger request for 2 seats, with the vehicle at 1 of 3 committed.
    What acted
    The passenger creates the request; the server computes an integer-paisa fare.
    What it decided
    Nothing yet.
    What changed
    B exists with status REQUESTED.
    What happens next
    A driver accepts it.

    Recorded in the project's own test run: request B, 2 seats.

  2. Pool matching: Pool matching: a new pool Recorded project run

    What came in
    The accepted request and the vehicle's current pools.
    What acted
    Pool matching.
    What it decided
    Join an existing pool when the routes are compatible, or found a new one.
    What changed
    In the recorded run, B ends up in Pool 2.
    What happens next
    The status moves through the transition table.

    Recorded in the project's own test run: request B's pool.

  3. State transition: The conditional accept Modelled from the code

    What came in
    A driver's accept.
    What acted
    The transition table and a conditional (compare-and-set) update.
    What it decided
    Move B from REQUESTED to MATCHED only if it is still REQUESTED.
    What changed
    The status can change, if the guard allows it.
    What happens next
    The capacity guard sums the vehicle's seats.

    Described from the implementation: every status change goes through one transition table.

  4. Capacity check: The guard counts: 1 + 2 = 3 Runs in browser

    What came in
    1 seats already committed across the vehicle's active pools, and 2 requested.
    What acted
    The capacity guard, inside the accept transaction.
    What it decided
    3 fits within 3, so the accept may proceed.
    What changed
    Nothing is blocked.
    What happens next
    The accept is written.

    The guard's arithmetic, run in your browser: 1 + 2 ≤ 3.

  5. Accept or refuse: Accepted: HTTP 200 Recorded project run

    What came in
    The guard's verdict.
    What acted
    The accept endpoint.
    What it decided
    Commit the accept.
    What changed
    B is MATCHED. The vehicle is at 3 of 3.
    What happens next
    A status event is recorded.

    Recorded in the project's own test run: request B returned HTTP 200.

  6. Status event: Event: SUCCESS Recorded project run

    What came in
    The outcome of the accept.
    What acted
    The status-event writer.
    What it decided
    Every transition leaves an audit event.
    What changed
    A SUCCESS event is committed with the state change.
    What happens next
    End of the trace.

    Recorded in the project's own test run: events SUCCESS, SUCCESS.

Request C is refused A sequential accept the guard refuses, recorded.

  1. Request: Request C asks for 2 seats Recorded project run

    What came in
    A passenger request for 2 seats, with the vehicle at 3 of 3 committed.
    What acted
    The passenger creates the request; the server computes an integer-paisa fare.
    What it decided
    Nothing yet.
    What changed
    C exists with status REQUESTED.
    What happens next
    A driver accepts it.

    Recorded in the project's own test run: request C, 2 seats.

  2. Pool matching: Pool matching Recorded project run

    What came in
    The accepted request and the vehicle's current pools.
    What acted
    Pool matching.
    What it decided
    Join an existing pool when the routes are compatible, or found a new one.
    What changed
    In the recorded run, C is placed in a pool before the capacity check refuses it.
    What happens next
    The status moves through the transition table.

    Recorded in the project's own test run: request C's pool.

  3. State transition: The conditional accept Modelled from the code

    What came in
    A driver's accept.
    What acted
    The transition table and a conditional (compare-and-set) update.
    What it decided
    Move C from REQUESTED to MATCHED only if it is still REQUESTED.
    What changed
    The status can change, if the guard allows it.
    What happens next
    The capacity guard sums the vehicle's seats.

    Described from the implementation: every status change goes through one transition table.

  4. Capacity check: The guard counts: 3 + 2 = 5 Runs in browser

    What came in
    3 seats already committed across the vehicle's active pools, and 2 requested.
    What acted
    The capacity guard, inside the accept transaction.
    What it decided
    5 is more than 3, so the accept is refused.
    What changed
    The transaction rolls back.
    What happens next
    A conflict is returned.

    The guard's arithmetic, run in your browser: 3 + 2 > 3.

  5. Accept or refuse: Refused: HTTP 409 Recorded project run

    What came in
    The guard's verdict.
    What acted
    The accept endpoint.
    What it decided
    Refuse the accept.
    What changed
    The car stays at 3 of 3. The project's message reads: "This vehicle is already carrying too many committed seats across its current trips". It refers to the accept that would exceed capacity.
    What happens next
    A status event is recorded.

    Recorded in the project's own test run: request C returned HTTP 409.

  6. Status event: Event: CONFLICT, written after the rollback Recorded project run

    What came in
    The outcome of the accept.
    What acted
    The status-event writer.
    What it decided
    Every transition leaves an audit event.
    What changed
    A CONFLICT event is written after the rollback, so the refusal is audited.
    What happens next
    End of the trace.

    Recorded in the project's own test run: events SUCCESS, SUCCESS, CONFLICT.

Two accepts at once The documented limit: both pass the guard.

  1. Request: Two accepts at the same moment Recorded project run

    What came in
    A is already accepted (1 of 3). B and C each ask for 2 seats, and two drivers accept at the same time.
    What acted
    Two accept requests run concurrently.
    What it decided
    Nothing yet.
    What changed
    Two transactions are open at once.
    What happens next
    Each reads the vehicle's pools.

    Recorded: an observation under the project's in-memory database mock, consistent with the code's read-then-write structure. PostgreSQL was not run.

  2. Capacity check: Both read the pools, and both see 1 of 3 Recorded project run

    What came in
    Each accept's read of the vehicle's active pools.
    What acted
    The capacity guard, in each transaction, reading before either has written.
    What it decided
    Each computes the committed seats from what it read: 1.
    What changed
    Neither transaction can see the other's pending change.
    What happens next
    Each checks its own total.

    Recorded: an observation under the project's in-memory database mock, consistent with the code's read-then-write structure. PostgreSQL was not run.

  3. Capacity check: Both pass: 1 + 2 = 3 each Runs in browser

    What came in
    1 committed seat and 2 requested, in each transaction.
    What acted
    The guard's arithmetic, once per transaction.
    What it decided
    Each total, 3, fits within 3. Both pass.
    What changed
    Both accepts are cleared to write.
    What happens next
    Both write.

    The guard's arithmetic, run in your browser: 1 + 2 ≤ 3, for each transaction separately.

  4. Accept or refuse: Both write: HTTP 200 twice Recorded project run

    What came in
    Two cleared accepts.
    What acted
    The accept endpoint, twice.
    What it decided
    Each commits.
    What changed
    B and C are both MATCHED.
    What happens next
    The vehicle is over capacity.

    Recorded: an observation under the project's in-memory database mock, consistent with the code's read-then-write structure. PostgreSQL was not run.

  5. Status event: 5 of 3: the boundary is crossed Recorded project run

    What came in
    The committed seats after both writes.
    What acted
    Nothing: no component notices.
    What it decided
    None. The guard ran twice and passed twice.
    What changed
    Five seats are committed on a three-seat vehicle, and both responses said 200.
    What happens next
    What would prevent this?
    Failure mode
    The guard reads, sums, then writes. Two transactions can read before either writes, so each check passes against a stale total.
    Caveat
    Observed under the mock's interleaving. Real PostgreSQL behavior was not run, so the exact interleaving there is unverified.

    Recorded: an observation under the project's in-memory database mock, consistent with the code's read-then-write structure. PostgreSQL was not run.

  6. A stronger version: A design note, not implemented: what a stronger version would need Modelled from the code

    What came in
    The failure above.
    What acted
    A design change that is not in the project.
    What it decided
    Make check-and-claim one indivisible step: serialize accepts per vehicle (lock the vehicle row or use a serializable transaction), or claim seats with a single conditional update on a committed-seat counter.
    What changed
    The second accept would see the first one's seats, or wait, and then be refused with a 409.
    What happens next
    Then test it against a real PostgreSQL with true concurrency, not a mock.

    A design note, not an implementation: nothing here is in the repository and nothing here was run.

Problem

Pooling riders into shared trips under constraints (pickup within 1.5 km, destination within 3 km, vehicle capacity) while several actors change state at once, with payments and an auditable history.

What I built

A Next.js front end, an Express API and a PostgreSQL schema accessed through Prisma. A ride request moves from REQUESTED to MATCHED, DRIVER_ARRIVED, STARTED and COMPLETED, and can be cancelled before it starts. Drivers form shared pools when a request is route-compatible, and every transition leaves a status event. Completion settles a simulated wallet or cash payment.

My contribution

Role
Sole contributor in the commit history.
Personal
All 125 commits and 15 merged pull requests, with iterative work on the core ride service (26 September to 1 October 2026).Basis: commit history.
Team
None.
Upstream
Framework scaffolding and standard libraries.
Development process
AI-assisted development: Claude Code was used for scaffolding, backend and frontend implementation, tests and git workflow, and a design plugin guided the visual direction. I made the architecture and design decisions, reviewed and corrected the output, and checked behavior by running the code. The repository keeps a log of what was accepted, rejected and fixed.
Provenance
Built in an assessment context; the assessment brief is not in the repository. The repository documents the AI tools it used in docs/ai-usage-notes.md.

Architecture

Shared vehicle capacity guardWhen one of three seats is committed, a two seat request fits and commits the remaining seats. When all three seats are committed, that request is refused at the capacity guard with HTTP 409.seats committed: 3 of 3ACCBBrequest Crequest C accepted: 2 seatsguard refuses: HTTP 409
The request fits, so it is accepted into free seats.The request would exceed the capacity, so it is refused with HTTP 409.Three seats are committed on a vehicle of capacity 3; a request for two more is refused at the capacity line, in a recorded test run.Modelled from the code
  1. A passenger creates a request, and the server computes an integer-paisa fare.
  2. A driver accepts it. A conditional update succeeds only if the request is still REQUESTED.
  3. The service joins an existing pool when routes are compatible, or founds a new pool.
  4. Inside one transaction it sums the seats already committed across the vehicle’s active pools and refuses the accept if the total would exceed capacity.
  5. Status transitions, including a conflict outcome, are written as audit events.
  1. Request

    A passenger creates a request with a seat count, and the server computes an integer-paisa fare.

    Decision
    Integer paisa for money.
  2. Transition table

    Every status change goes through one table: REQUESTED, MATCHED, DRIVER_ARRIVED, STARTED, COMPLETED, with cancellation allowed before the ride starts.

    Why it exists
    So no route can skip a state.
    Decision
    One transition table for every status change.
    Evidence
    Verified 71 written test cases exist in the repository. They were counted, not run for this page.
    Source
    apps/api/src/common/rideLifecycle.ts
  3. Conditional accept

    A driver accepts a request. A conditional update succeeds only if the request is still REQUESTED.

    Decision
    Conditional (compare-and-set) updates for single-row races.
    Limitation
    It covers a single row, not the vehicle's total across pools.
  4. Pool matching

    The service joins an existing pool when routes are compatible, or founds a new pool.

    Why it exists
    Pools form by route compatibility, within pickup and destination distance limits.
  5. Capacity guard

    Adds up the seats committed across every active pool of the vehicle (open or locked) and compares that total plus the new request with the vehicle's capacity.

    Input
    Requested seats (1 to 6) and the vehicle's active pools.
    Output
    Accept, or HTTP 409 and a conflict status event.
    Why it exists
    A per-pool check passed while the vehicle as a whole was over capacity, so the guard was moved to the vehicle level.
    Decision
    Guard the vehicle's total, not each pool.
    Control
    The accept transaction refuses when the total plus the request exceeds capacity.
    Failure mode
    Two accepts at the same moment can both read a total under capacity before either writes.
    Limitation
    Concurrent accepts across pools are not covered by this guard.
    Evidence
    Verified Recorded before and after the guard commit: 5 of 3 seats without it, 3 of 3 with it. In-memory database mock, not PostgreSQL.
    Evidence
    Partially verified Two accepts at the same moment both passed the guard under the mock, reaching 5 of 3. PostgreSQL behavior was not run.
    Source
    apps/api/src/modules/rides/rides.service.ts, accept transaction
  6. Status events

    Every transition leaves a status event, and a conflict outcome is written as an event after the rollback.

    Decision
    Success events committed with state changes, and conflict events written after a rollback.

Engineering decisions

  • One transition table for every status change, so no route can skip a state.
  • Conditional (compare-and-set) updates for single-row races.
  • Identity from the verified token, with ownership checks.
  • Success events committed with state changes, and conflict events written after a rollback.
  • An aggregate guard across pools, added after an overbooking was reported, because guarding one pool does not protect the vehicle.
  • Integer paisa for money, and one vehicle per driver by a unique constraint.

Evaluation

Seven backend test files define 71 test cases (52 for rides). There are no frontend, end-to-end or database-backed tests, and no executed results are retained. The recorded behavior shown in the trace below was produced by running the project’s own tests at two commits.

Results

VerifiedA lifecycle transition table and 71 test cases across seven backend test files are defined in the repository.Read the source and counted the test cases; passing is not claimed.

VerifiedThe third accept gives different results before and after the capacity guard commit. Without the guard it returns HTTP 200 and the vehicle holds 5 of 3 seats; with it, HTTP 409 and 3 of 3 seats.Ran the project's own test harness on both commits and recorded the states. Uses the project's in-memory database mock, not PostgreSQL.

Partially verifiedTwo accepts made at the same moment can both pass the capacity guard.Ran two simultaneous accepts under the harness. Observed under the mock's interleaving and consistent with the code's read-then-write structure; real PostgreSQL behavior was not run.

VerifiedCompleting one rider marks the whole pool completed, and cancellation does not release seats.Read the source.

VerifiedEvery data-touching test mocks the database; the largest suite implements an in-memory database with artificial interleaving.Read the test setup.

Not claimedThe README states that race conditions were checked against a live database and that tests pass. No results are retained, so neither is claimed here.Looked for retained results and found none.

Limitations

  • The project is a local-only MVP with fixed seeded zones, straight-line distances and one vehicle per driver.
  • The capacity guard reads, sums, then writes, and concurrent behavior against a real database was not run.
  • Completing one rider completes the whole pool, and cancellation never releases seats.
  • Tests mock the database, there are no end-to-end or database-backed tests, and there is no CI or deployment.

Not claimed Production readiness, scale and performance are not claimed for this project.

Engineering Trace

A vehicle has 3 seats. Three requests of 1, 2 and 2 seats are accepted one after another, each into its own pool. What stops the vehicle from carrying 5?

Recorded project run

Partially verifiedStates were recorded by running the project's own jest tests and service code at the commit before the capacity guard and at the commit that added it; this site did not generate or simulate the states. The first state condenses the creation of the three requests into one step. Database: In-memory mock (not PostgreSQL).

Also known: Two accepts at the same momentPartially verified
Two accepts overlapping in timeAccept B and accept C each read the vehicle's pools, then each writes. If both reads happen before either write, both see three or fewer committed seats.accept Bread poolswriteaccept Cread poolswriteboth reads happen before either write

The guard reads, sums, then writes. In the project's test harness, accepting B and C at the same moment (A already accepted) let both through: 5 of 3 seats, both HTTP 200.

An observation under the mock's interleaving, consistent with the code's read-then-write structure. Real PostgreSQL behavior was not run.

The trace in text

  1. Capability. The service accepts ride requests into shared pools and is designed not to commit more seats to one vehicle than the vehicle has.
  2. Expected behavior. Requests A (1 seat) and B (2 seats) fill the 3-seat vehicle. Each pool is individually under capacity, and a third request of 2 seats must not be admitted.
  3. Trigger. Request C (2 seats, a pickup too far from A and B to share a pool) is accepted after A and B.
  4. Recorded states.
    1. The vehicle has 3 seats. Requests A (1 seat), B (2 seats) and C (2 seats) are waiting. Their pickups are 4.5 km or more apart, so none can share a pool. Recorded: events none yet; requests A REQUESTED, B REQUESTED, C REQUESTED.
    2. Request A is accepted. A founds its own pool. Committed seats: 1 of 3. Recorded: HTTP 200; events SUCCESS; requests A MATCHED in Pool 1, B REQUESTED, C REQUESTED.
    3. Request B is accepted. B also founds its own pool. Committed seats: 3 of 3. Each pool is individually under capacity. Recorded: HTTP 200; events SUCCESS, SUCCESS; requests A MATCHED in Pool 1, B MATCHED in Pool 2, C REQUESTED.

    With the guard

    1. Request C meets the guard. The accept transaction sums the vehicle's active pools (3) and finds that 3 + 2 is more than 3. HTTP 409. C stays REQUESTED and no pool is created. Recorded: HTTP 409 (This vehicle is already carrying too many committed seats across its current trips); events SUCCESS, SUCCESS, CONFLICT; requests A MATCHED in Pool 1, B MATCHED in Pool 2, C REQUESTED.

    Without the guard

    1. Request C is accepted. C founds a third pool. Committed seats: 5 of 3, more than the vehicle has. Recorded at the commit before the guard: HTTP 200. Recorded: HTTP 200; events SUCCESS, SUCCESS, SUCCESS; requests A MATCHED in Pool 1, B MATCHED in Pool 2, C MATCHED in Pool 3.
  5. Control. The accept transaction sums the seats already committed across all of the vehicle's active pools before it commits, and refuses the accept when the total would exceed capacity.
  6. Measurement. Committed seats on the vehicle after the third accept, the HTTP status returned, and the audit events written.
  7. Outcome. With the guard: HTTP 409, C stays REQUESTED, a conflict event is recorded and the vehicle holds 3 of 3 seats. Without it: HTTP 200 and the vehicle holds 5 of 3 seats.
  8. Limitation. The guard reads the pools, sums the seats, then writes. Two accepts at the same moment can both pass the check; this was observed under the test harness and has not been run against a real database.

Evidence

  • Verified The third accept gives different results before and after the guard commit: HTTP 200 and 5 of 3 seats before, HTTP 409 and 3 of 3 seats after. Recorded by running the project's own test harness on the project's own service code at both commits, with the project's in-memory database mock.
  • Partially verified Two accepts made at the same moment can both pass the guard. Observed under the mock's interleaving (5 of 3 seats, both HTTP 200). Real PostgreSQL transaction behavior was not run.

Commits: 3af09ba (before the guard), 54de79d (adds the guard). Test: "caps total committed seats across ALL of a Tesla's active pools, not just the one pool being joined or founded".

Takeaways

Guarding one row does not guard an invariant that spans rows. The aggregate guard fixed the sequential overbooking and also showed what remains: a check that reads and then writes still needs a stronger mechanism under concurrency.

Tests that mock the database validate logic under chosen interleavings, not database isolation.

Repository