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)
| Question | Why it matters |
|---|---|
| Docked bikes only, or also dockless? |
|
| 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)
- Payment gateway processing and merchant settlement (covered in Payment Gateway)
- City permit and station placement planning
- Manufacturing dock hardware
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.
| Metric | Calculation | Value |
|---|---|---|
| Total bikes | Given | 50,000 |
| Total stations | Given | 5,000 |
| Docks per station | Given | 10-40 (avg 15) |
| Daily trips | Given | 500K |
| Trips / sec peak | Derived from daily volume ÷ 86400 (+ peak factor) | ~50 (rush hour) |
| Active rides concurrently | Given | ~25,000 |
| Station status updates / sec | Derived 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.
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 secondsBike 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
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
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
GET /api/v1/stations?lat=37.7749&lng=-122.4194&radius=1000&limit=10Reserve a Bike
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
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
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
| Concern | Solution |
|---|---|
| Race condition on checkout | Redis SET NX (atomic lock) |
| Dock controller offline |
|
| Redis failure |
|
| Payment failure |
|
| Trip not ended (abandoned) |
|
| Network outage at station |
|
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
How helpful was this walkthrough?
Click a star to rate. We actively use this feedback to refine and update our system design content.
Discussion
Share your thoughts, ask questions, or help others.