System Design Problem

Design a Bike Sharing System like Citi Bike

Commonly Asked By:UberLyftLime

Interview Setup

Interview Prompt

Design a bike sharing system like Citi Bike. Users find nearby stations, unlock a bike, ride, and dock at any station. The system tracks real-time dock availability, handles membership pricing, and keeps stations balanced across the city.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Docked bikes only, or also dockless?
  • Docked systems have station inventory and MQTT dock controllers
  • dockless shifts to GPS on the bike.
What is the peak checkout rate?500K daily trips represents about 50/sec at rush hour, so while QPS is low, race conditions on the last bike at a station matter.
How does the physical unlock work?MQTT to dock hardware adds 2 to 3s latency and timeout handling to the critical path.
Is rebalancing in scope?The morning commute empties residential stations, requiring a prediction pipeline for operations or customer incentives.

Scope

In scope

  • Station map with real-time availability
  • Bike checkout with distributed lock
  • Dock-sensor return and fare calculation
  • Membership and time-based pricing
  • 5-minute bike reservation
  • Station rebalancing predictions

Out of scope (state explicitly)

Functional Requirements

Confirm the unlock workflow, station inventory boundaries, and rebalancing scope. Key capabilities include discovering nearby bikes, locking and unlocking hardware docks, calculating trip fares, and tracking station capacity.

In the interview: two users unlocking the same bike simultaneously is a classic race condition, so highlight distributed locks or conditional updates immediately.

  • Station map: Show nearby docking stations with availability
  • Rent a bike: Unlock a bike from a station
  • Return a bike: Dock at any station; end the trip
  • Pricing: Time-based pricing with free minutes for members
  • Membership: Annual/monthly and single-ride passes
  • Trip history: View past rides
  • Rebalancing alerts: Notify when stations are too full/empty
  • Station status: Real-time dock availability
  • Reservation: Reserve a bike for 5 minutes
  • E-bike support: Battery level tracking, premium pricing

Non-Functional Requirements

Unlocking must complete within a few seconds while station bike counts maintain high accuracy. Concurrent unlocks on the last bike require strict consistency.

  • Effectively-once checkout: Reservation locks and idempotency keys ensure duplicate checkout requests return the original reservation.
  • Low Latency: Bike unlock in < 3 seconds
  • Real-time Station Data: Updates within 10 seconds
  • Availability: 99.9%: downtime means stranded riders
  • Scalability: 50K+ bikes, 5K+ stations, 500K+ daily trips
  • Durability: Trip records for billing must never be lost
  • Fault Tolerant: Bikes unlockable during partial outages

Capacity Estimations

Fleet size, docking stations, and daily trip volume determine the storage footprint for trip records and the network throughput for GPS telemetry updates.

MetricCalculationValue
Total bikesGiven50,000
Total stationsGiven5,000
Docks per stationGiven10-40 (avg 15)
Daily tripsGiven500K
Trips / sec peakDerived from daily volume ÷ 86400 (+ peak factor)~50 (rush hour)
Active rides concurrentlyGiven~25,000
Station status updates / secDerived from daily volume ÷ 86400 (+ peak factor)~100
Bike heartbeat (e-bikes)Given~1,700/sec

Architecture Diagram

The architecture coordinates mobile clients, physical docking stations, and cloud backend microservices. Dock controllers stream hardware telemetry and lock confirmations over MQTT, while riders query station availability and initiate trips over HTTPS. Redis coordinates distributed checkout locks and real-time station availability counts, PostgreSQL provides ACID durability for trips and financial ledgers, and Kafka streams trip events downstream to billing, notifications, and analytics pipelines. For background on geofence proximity and spatial telemetry, see the Geofencing Service and the Real-Time Vehicle Tracking problem.

Loading...

Component Deep Dives

Checkout and Return State Machines

The user journey transitions through discovery, reservation, unlock, active ride, and return. The last bike at a station represents the primary consistency pivot. Checkout and return represent the two critical state machines in the system. Checkout races resolve atomically through a Redis NX distributed lock, whereas return is driven strictly by dock hardware sensor events rather than the rider's mobile client. Supporting domains, such as pricing, billing, and rebalancing, operate asynchronously over event streams and can tolerate latency of several seconds without degrading user experience. For coordinating multi-stage checkout steps reliably, review Distributed Transactions: 2PC vs Saga.

Bike Checkout (Rent) Flow: The Critical Path

User taps "Unlock Bike" at Station 42, Dock 7:

Step 1: Validate user (auth, membership, balance, no active ride)

Step 2: Reserve the bike (prevent race condition)
  Redis: SET bike_lock:{bike_id} {user_id} NX EX 30
  NX = only one user wins. EX = 30s auto-release.

Step 3: Unlock the bike
  Send unlock command via MQTT to dock controller
  Wait for ACK (timeout 10s, retry once)

Step 4: Start trip
  INSERT INTO trips (...) VALUES (..., 'active')

Step 5: Update station availability
  DECR station:{id}:bikes_available
  INCR station:{id}:docks_available

Total latency: 2-3 seconds

Bike Return (Dock) Flow

1. Dock sensor detects bike inserted → MQTT event
2. Trip Service: find active trip → complete it → calculate fare
3. Lock the bike mechanism → release Redis lock
4. Update station availability (INCR bikes, DECR docks)
5. Publish "trip-completed" → Billing Service charges user
6. E-bikes: start battery charging

Station Rebalancing: The Operational Challenge

Commuters ride from residential to business districts.
By 9 AM: residential EMPTY, business FULL.

Strategies:
  1. Truck-based (reactive): VRP solver to redistribute bikes
  2. Incentive-based ⭐: "Bike Angels" program: points for returning to empty stations
  3. Predictive: ML predicts demand per station per hour → pre-position bikes

Rebalancing Service (every 5 min):
  For each station: delta = predicted_demand - current
  Sort by abs(delta) → generate truck plan → push to Ops Dashboard

API Design

Rental and Station Management APIs

The system exposes RESTful endpoints for discovering stations, securing reservations, unlocking docks, and completing returns. Idempotency keys protect critical checkout operations against network retries.

Rent a Bike

HTTP
POST /api/v1/trips/start
{
  "station_id": "stn-42",
  "dock_id": "dock-7",
  "bike_type": "classic"
}
Response: 200 OK
{
  "trip_id": "trip-uuid",
  "bike_id": "B-123",
  "started_at": "2025-03-14T08:30:00Z",
  "pricing_plan": "annual_member",
  "free_minutes": 30,
  "unlock_status": "success"
}

Return a Bike

HTTP
POST /api/v1/trips/end
{
  "trip_id": "trip-uuid",
  "station_id": "stn-55",
  "dock_id": "dock-12"
}
Response: 200 OK
{
  "trip_id": "trip-uuid",
  "duration_minutes": 22,
  "distance_km": 4.2,
  "fare": 0.00,
  "fare_breakdown": { "base": 0, "overage": 0, "ebike_premium": 0 }
}

Get Nearby Stations

HTTP
GET /api/v1/stations?lat=37.7749&lng=-122.4194&radius=1000&limit=10

Reserve a Bike

HTTP
POST /api/v1/reservations
{
  "station_id": "stn-42",
  "bike_type": "classic"
}
Response: 200 OK
{
  "reservation_id": "res-uuid",
  "station_id": "stn-42",
  "bike_id": "B-123",
  "expires_at": "2025-03-14T08:35:00Z"
}

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
440 Login Timeout: WebSocket connection session expired, client reconnect is required

Data Model

PostgreSQL

SQL
CREATE TABLE stations (
    station_id UUID PRIMARY KEY,
    name VARCHAR(255), lat DECIMAL(10,7), lng DECIMAL(10,7),
    total_docks SMALLINT, status ENUM('active','maintenance','closed'),
    geometry GEOMETRY(Point, 4326)
);
CREATE INDEX idx_location ON stations USING GIST(geometry);

CREATE TABLE bikes (
    bike_id UUID PRIMARY KEY,
    bike_type ENUM('classic','ebike'),
    status ENUM('available','rented','maintenance','retired'),
    current_station UUID REFERENCES stations,
    battery_level DECIMAL(3,2), total_trips INT DEFAULT 0
);

CREATE TABLE trips (
    trip_id UUID PRIMARY KEY,
    user_id UUID NOT NULL, bike_id UUID NOT NULL,
    start_station_id UUID NOT NULL, end_station_id UUID,
    started_at TIMESTAMPTZ NOT NULL, ended_at TIMESTAMPTZ,
    duration_minutes DECIMAL(8,2), distance_km DECIMAL(8,2),
    fare_cents INT, status ENUM('active','completed','cancelled'),
    INDEX idx_user (user_id, started_at DESC),
    INDEX idx_bike (bike_id, started_at DESC),
    INDEX idx_active (status) WHERE status = 'active'
);

CREATE TABLE users (
    user_id UUID PRIMARY KEY, email VARCHAR(255) UNIQUE,
    name VARCHAR(100), membership_type ENUM('annual','monthly','day_pass','none'),
    membership_expires DATE, balance_cents INT DEFAULT 0
);

Redis: Real-time State

station:{id}:bikes_available → INT
station:{id}:docks_available → INT
station:{id}:ebikes_available → INT
bike_lock:{bike_id} → user_id (TTL: 30s)
reservation:{station_id}:{bike_id} → reservation_id (TTL: 300s)
active_ride:{user_id} → trip_id (TTL: 86400)
ebike_battery:{bike_id} → FLOAT (TTL: 120)
stations:geo → Sorted Set (GEOADD for nearby queries)

ClickHouse: Trip Analytics

SQL
CREATE TABLE trip_analytics (
    trip_id UUID, user_id UUID, bike_id UUID,
    bike_type Enum8('classic'=0,'ebike'=1),
    start_station UUID, end_station UUID,
    started_at DateTime, ended_at DateTime,
    duration_min Float32, distance_km Float32, fare_cents UInt32,
    day_of_week UInt8, hour_of_day UInt8,
    trip_date Date MATERIALIZED toDate(started_at)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(started_at)
ORDER BY (start_station, started_at);

Event Bus Design (Kafka)

Topic: trip-completed
  Partitions: 32
  Partition key: trip_id
  Retention: 7 days

Topic: station-status-changes (dock sensor events via MQTT bridge)
  Partition key: station_id

Consumer groups:
  1. billing: charge user asynchronously on trip-completed (idempotent by trip_id)
  2. rebalancing: update demand forecast in ClickHouse
  3. analytics: trip duration, route, and fare metrics

Sync path: dock return → MQTT → Trip Service → UPDATE trips → publish trip-completed → fare shown < 2s
Async path: billing charge may take 5s; idempotent on duplicate sensor events

Fault Tolerance

ConcernSolution
Race condition on checkoutRedis SET NX (atomic lock)
Dock controller offline
  • Queue command
  • retry when online
  • manual override at kiosk
Redis failure
Payment failure
  • Start ride anyway (pre-authorized)
  • charge async post-ride
Trip not ended (abandoned)
  • Auto-end after 24h
  • charge max fare
  • flag for ops
Network outage at station
  • Local unlock capability (cached member list)
  • sync when online

Additional Considerations

Interview Walkthrough

  • 25-minute cut

    Skip arch50/arch75 depth unless staff.

    • Start with physical-digital boundary: app unlock command must reach the dock via MQTT (5 min)
    • Rent flow: find nearby stations (PostGIS) to atomic Redis NX lock on bike to charge and start trip (6 min)
    • Station inventory as source of truth: dock sensors reconcile available bike counts (5 min)
    • Return flow: validate dock availability, end trip, compute fare, release lock (5 min)
    • Staff only: rebalancing trucks as async operations: predict shortages rather than blocking the hot unlock path (4 min)
  • Start with the physical-digital boundary: unlocking a dock solenoid is just as critical as the API layer, so plan for hardware failure modes upfront.
  • Walk through the rent flow: find nearby stations using PostGIS, acquire an atomic Redis NX lock on the bike, decrement available counts, and open the trip record inside an ACID PostgreSQL transaction.
  • Explain station inventory as the source of truth: reconcile dock sensors with e-bike GPS telemetry, using confidence scores whenever sensors disagree.
  • Cover the return flow: validate dock availability, compute fares from duration and active plans, and release locks atomically across trips and station records.
  • Discuss rebalancing as an asynchronous operations problem: predict empty and full stations and dispatch rebalancing trucks without blocking the checkout or return hot paths.
  • Detail MQTT connections to station controllers for lock commands, along with local fallback mechanisms such as RFID member card authentication when cellular connectivity drops.
  • Address the common pitfall of treating station bike counts as eventually consistent: concurrent race conditions when renting the last remaining bike will trigger double-unlock failures or strand riders without strong consistency.

Dock Sensor Failure: "Ghost Bikes"

Detection: user reports + GPS check (e-bikes report real location)
Mitigation: show "reported" count, under-report, confidence scores
Opposite: "Phantom docks": kiosk override + maintenance alert

Handling Full / Empty Stations

Full station: extra 15 free min + show nearby alternatives
Virtual return: mark "returned" at full station (risk: theft)
Push notification: "Station 55 is filling up; consider Station 57"

Physical Lock Mechanism

App → API → MQTT → Station Controller → Dock Lock Motor

Failure modes:
  a. Solenoid stuck → "dock unavailable" + alert maintenance
  b. Cellular outage → fallback: local member card auth
  c. Power outage → battery backup (4hr) + fail-secure (bikes stay locked)

This is why 99.9% (not 99.99%): physical hardware fails more than software

Engineering Trade-offs

Inventory Allocation and Database Trade-Offs

Designing shared micro-mobility systems requires balancing inventory predictability against hardware constraints, choosing between optimistic or pessimistic locking, and picking transactional stores that support rapid checkout flows.

Should You Allow Reservations?

Pros: user knows bike is waiting, reduces frustration
Cons: reduces effective supply, no-shows waste 5 min

Citi Bike: NO reservations for regular bikes. E-bikes can be reserved (premium).

If implementing: max 1 per user, 5-min window, limit to stations with ≥5 bikes

Why PostgreSQL (Not DynamoDB/Cassandra) for Trips?

ACID required: lock + start trip + update station (multi-table atomic)
Billing queries: SUM fares by user (JOINs + GROUP BY)
Moderate scale: 500K trips/day = ~6 writes/sec

PostgreSQL ✓: ACID, PostGIS, rich analytics, 6 writes/sec trivial
DynamoDB ✗: 25-item tx limit, no JOINs
Cassandra ✗: overkill, no transactions, no JOINs

At Citi Bike scale: PostgreSQL is the right choice.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...