SLIDE 115: Section Intro, API Design
Section 9: API Design

The API layer is the contract between clients and the system. Every action a rider or driver takes in the app translates to an API call. Requesting a ride is a POST. Checking ETA is a GET. Cancelling is a DELETE. Accepting a dispatch is a WebSocket event. The API design determines how clients interact with the services we built in Section 4 and the data flows we traced in Section 5.
In an interview, API design is not about listing every endpoint. It is about showing that you understand the interaction model: which operations are REST request-response, which are real-time push over WebSocket, how authentication works at the edge, how rate limiting protects backend services, and why cursor pagination is the right choice for real-time datasets.
What We’ll Cover:
- Rider-facing APIs: endpoints, request/response examples, error handling
- Driver-facing APIs: REST endpoints and WebSocket events
- Real-time push mechanisms: WebSocket vs push notifications vs polling
- Auth and rate limiting at the API Gateway
- Cursor pagination and API versioning
This section connects backward to Section 4 (Architecture), where the API Gateway and WebSocket Gateway were defined, and Section 8 (Database Design), where the schemas these APIs read from and write to were specified.
SLIDE 116: Rider-Facing APIs
Core Rider Endpoints
The rider interacts with the system through five primary endpoints. Each maps to a functional requirement from Section 2.
POST /v1/rides Request a new ride
GET /v1/rides/{id} Get ride status and details
GET /v1/rides/{id}/eta Get current ETA for an active ride
DELETE /v1/rides/{id} Cancel a ride
POST /v1/rides/{id}/rating Rate the driver after trip completion
GET /v1/rides/history Paginated trip history
Why These Methods
POST for ride requests because creating a ride is a state-changing operation that creates a new resource. GET for status and ETA because these are read-only. DELETE for cancellation because it terminates the resource’s active lifecycle. POST for rating because it creates a new rating record associated with the ride.
What About Real-Time Updates?
The rider also receives real-time updates during an active ride: driver location, ETA changes, state transitions (driver matched, driver arriving, trip started). These are not REST endpoints. They are pushed to the rider app via the WebSocket connection established when the ride becomes active. The rider app opens a WebSocket to receive updates and uses REST for actions (request, cancel, rate).
This separation is deliberate. Actions are request-response (REST). Updates are server-pushed (WebSocket). Mixing these models into a single protocol would complicate both the client and the server.
SLIDE 116B: Rider API, Request and Response Examples
POST /v1/rides: Request a Ride
// Request
{
"pickup": { "lat": 40.7484, "lng": -73.9857 },
"dropoff": { "lat": 40.7614, "lng": -73.9776 },
"vehicle_type": "standard"
}
// 201 Created
{
"ride_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "requested",
"estimated_fare": { "amount": 18.50, "currency": "USD", "surge": 1.0 },
"estimated_pickup_eta_seconds": 240,
"created_at": "2026-09-01T14:30:00Z"
}
The response returns the ride ID (used for all subsequent calls), the initial status, the fare estimate shown to the rider before confirmation, and the estimated pickup time.
GET /v1/rides/{id}: Ride Status
// 200 OK
{
"ride_id": "a1b2c3d4-...",
"status": "driver_en_route",
"driver": { "name": "Priya", "rating": 4.87, "vehicle": "Toyota Camry, MH-02-AB-1234" },
"pickup_eta_seconds": 180,
"fare_estimate": { "amount": 18.50, "currency": "USD" }
}
Error Responses
// 404: No drivers available
{ "error": "no_drivers_available", "message": "No drivers found nearby. Please try again." }
// 409: Active ride already exists
{ "error": "active_ride_exists", "message": "You already have an active ride.", "ride_id": "..." }
// 429: Rate limited
{ "error": "rate_limited", "message": "Too many requests. Please wait.", "retry_after_seconds": 30 }
Error responses use standard HTTP status codes with a machine-readable error code and a human-readable message. The retry_after_seconds field on 429 tells the client exactly when to retry, preventing unnecessary retries.
SLIDE 117: Driver-Facing APIs
Core Driver Endpoints
The driver’s API is a mix of REST endpoints for actions and WebSocket events for real-time communication.
POST /v1/drivers/status Go online or offline
GET /v1/drivers/status Get current status
POST /v1/dispatch/{id}/accept Accept a ride offer
POST /v1/dispatch/{id}/decline Decline a ride offer
POST /v1/trips/{id}/arrive Mark arrival at pickup
POST /v1/trips/{id}/start Start the trip
POST /v1/trips/{id}/complete Complete the trip
GET /v1/drivers/earnings Earnings summary
Why POST for Status Changes Instead of PUT
Going online is not updating a resource property. It triggers a cascade of system actions: opening a WebSocket connection, adding the driver to the spatial index, making them eligible for dispatch. POST signals “this is an action with side effects,” not just a field update. The same logic applies to trip lifecycle endpoints (arrive, start, complete): each triggers downstream processing beyond a simple status field change.
Driver Actions vs Driver Events
The endpoints above are actions the driver initiates. But the driver also receives events pushed by the system: a new dispatch offer, a ride cancellation by the rider, ETA updates during navigation. These arrive over the WebSocket connection, not as REST responses. The next slide covers the WebSocket event model.
SLIDE 117B: Driver API, Request and Response Examples
POST /v1/drivers/status: Go Online
// Request
{
"status": "online",
"location": { "lat": 40.7484, "lng": -73.9857 }
}
// 200 OK
{
"status": "online",
"websocket_url": "wss://ws.rideshare.com/v1/driver?token=eyJhbG...",
"message": "You are now online and visible to riders."
}
The response includes a WebSocket URL with an embedded auth token. The driver app connects to this URL immediately after going online. All subsequent real-time communication flows through this connection.
WebSocket Events the Driver Receives
Once connected, the driver receives server-pushed events as JSON messages over the WebSocket.
// Dispatch offer
{
"event": "dispatch_offer",
"dispatch_id": "d1e2f3...",
"pickup": { "lat": 40.7490, "lng": -73.9860 },
"pickup_address": "350 5th Ave, New York",
"estimated_distance_km": 8.2,
"estimated_fare": { "amount": 22.00, "currency": "USD" },
"pickup_eta_seconds": 300,
"expires_in_seconds": 15
}
// Ride cancelled by rider
{
"event": "ride_cancelled",
"ride_id": "a1b2c3d4-...",
"reason": "rider_cancelled",
"cancellation_fee_earned": 5.00
}
The expires_in_seconds field on the dispatch offer tells the driver app to show a countdown timer. When the timer hits zero, the app disables the accept button. The server independently expires the offer and cascades to the next driver. Both sides enforce the timeout independently so that network delays do not create inconsistent state.
SLIDE 118: Real-Time Push, WebSocket Events vs Push Notifications
Two Push Mechanisms, Different Use Cases
The system pushes data to clients through two channels: WebSocket for connected, active clients, and push notifications for backgrounded or disconnected clients. They are not interchangeable.
WebSocket: Continuous, Bidirectional, Low Latency
Used when the client is actively in the app and needs a continuous stream of updates. Driver location during an active ride, ETA updates every few seconds, dispatch offers that need a response within 15 seconds. WebSocket connections are persistent, bidirectional, and deliver messages in tens of milliseconds.
Push Notifications: One-Shot Alerts, Higher Latency
Used when the app is backgrounded or closed. “Your driver has been matched,” “your driver is arriving,” “your trip is complete,” “your receipt is ready.” Push notifications go through Apple’s APNs or Google’s FCM, adding 1-5 seconds of latency. They are one-way (server to client only) and best suited for alerts that do not require an immediate in-app response.
SLIDE 118B: When to Use WebSocket vs Push vs Polling
Three Options, Clear Decision Criteria
WebSocket
Use when the client needs continuous real-time data and is actively connected. The connection stays open for the duration of the interaction (driver’s shift, rider’s active ride). The server pushes data whenever it changes, with no client-side polling overhead.
Cost: each WebSocket connection consumes server memory and a file descriptor. At 5 million concurrent driver connections, the WebSocket Gateway must handle significant connection state. This is why the WebSocket Gateway is a separate component from the API Gateway (Section 4).
Push Notifications (APNs/FCM)
Use when the client is not actively connected but needs to be alerted. The message goes through a third-party service (Apple or Google), which wakes the app or shows a banner. Latency is 1-5 seconds. Delivery is not guaranteed (notifications can be dropped if the device is offline for too long).
Cost: per-message pricing from the notification platform, plus the complexity of maintaining device tokens and handling token refresh.
Short Polling (Fallback)
Use only when WebSocket connections fail. The client polls the server every N seconds for updates. This is the least efficient option: at 5 million drivers polling every 4 seconds, the system handles 1.25 million HTTP requests per second just for location updates, each with full HTTP overhead (headers, connection setup, TLS handshake).
In the main design, polling is a degraded fallback, not a primary mechanism. If the interviewer asks “what if the client cannot maintain a WebSocket connection,” short polling with exponential backoff is the answer.
SLIDE 119: Auth Service, JWT Validation and OAuth
Authentication at the Edge
Every request that hits the API Gateway must be authenticated before it reaches any backend service. The Auth Service validates the caller’s identity and rejects unauthenticated requests at the edge, preventing unauthorized traffic from consuming backend resources.

JWT Structure
The system uses JSON Web Tokens (JWT) for stateless authentication. When a rider or driver logs in, the Auth Service issues a signed token containing their identity.
{
"sub": "user_a1b2c3d4",
"role": "rider",
"city": "mumbai",
"exp": 1725235200,
"iss": "rideshare-auth"
}
The sub field identifies the user. The role field determines whether this is a rider, driver, or admin (used for authorization, not just authentication). The city field can be used by the API Gateway for geographic routing. The exp field is the expiration timestamp.
Token Refresh for Long Driver Sessions
A driver’s shift can last 8-12 hours. A JWT with a 1-hour expiration would require the driver to re-authenticate during a shift, potentially interrupting an active ride. The solution is a refresh token flow: the client uses a short-lived access token (1 hour) for API calls and a long-lived refresh token (30 days) to obtain new access tokens without re-entering credentials.
OAuth for Third-Party Integrations
Social login (Google, Apple Sign-In) uses OAuth 2.0. Payment provider integrations (Stripe, Braintree) use OAuth for token-based authorization to charge the rider’s payment method. The Auth Service acts as the OAuth client, handling the redirect flow and storing the resulting tokens securely.
SLIDE 120: Rate Limiter, Per-Endpoint and Per-User
Why Rate Limiting Matters for a Ride-Sharing System
Without rate limiting, a single abusive client could flood the system with ride requests, consuming matching capacity and spatial index queries that should serve legitimate riders. Rate limiting protects backend services from abuse, accidental client bugs (infinite retry loops), and deliberate attacks.
Three Levels of Rate Limiting
Per-user, per-endpoint: the most granular level. A rider can submit at most 5 ride requests per minute. A driver can submit at most 1 status change per 10 seconds. These limits prevent individual abuse without affecting other users.
Per-user aggregate: a cap across all endpoints for a single user. Even if individual endpoints allow 5 requests/minute each, a single user cannot exceed 100 total requests per minute across all endpoints. This catches abuse patterns that spread across endpoints.
Global surge protection: a system-wide limit during extreme spikes. If total incoming requests exceed a threshold (e.g., 10x normal peak), the API Gateway starts shedding low-priority traffic. Ride requests and active trip operations are prioritized. Trip history queries and earnings lookups are throttled.
Implementation
Rate limiting is implemented at the API Gateway using a sliding window counter stored in Redis. Each user-endpoint combination has a key (e.g., ratelimit:rider_123:POST:/v1/rides) with a counter that increments on each request and resets after the window expires. When the counter exceeds the limit, the Gateway returns 429 with a retry_after_seconds header.
SLIDE 121: Cursor Pagination for Trip History
Why Cursor, Not Offset
Offset pagination (?page=3&limit=20) breaks when the underlying data changes between requests. If a new trip completes while the rider is browsing page 2, page 3 will duplicate one of the trips from page 2 (the entire dataset shifted by one). In a system where 20 million new trips are created daily, this shift happens constantly.
Cursor pagination uses a pointer to a specific record rather than a position in the list. “Give me 20 trips after this cursor” always returns the correct next batch, regardless of how many new trips were inserted since the last request.
Request and Response
// Request
GET /v1/rides/history?limit=20&cursor=eyJ0IjoiMjAyNi0wOC0zMVQxMDowMDowMFoiLCJpZCI6ImE...
// 200 OK
{
"rides": [
{
"ride_id": "f5e6d7c8-...",
"pickup": "350 5th Ave",
"dropoff": "JFK Airport",
"fare": 52.00,
"status": "completed",
"completed_at": "2026-08-30T18:45:00Z"
}
],
"next_cursor": "eyJ0IjoiMjAyNi0wOC0zMFQxODo0NTowMFoiLCJpZCI6ImY..."
}
The cursor is an opaque, base64-encoded string containing the last seen created_at timestamp and ride_id. The client does not parse it. It passes the next_cursor from the response into the next request. When next_cursor is null, there are no more results.
The Underlying Query
SELECT * FROM trips
WHERE rider_id = $1
AND (created_at, id) < ($cursor_timestamp, $cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
This query uses the composite index (rider_id, created_at DESC) from Section 8. The cursor condition (created_at, id) < (...) ensures stable pagination even when multiple trips share the same timestamp.
SLIDE 122: API Versioning and Backward Compatibility
Why Versioning Matters
The rider app and driver app are mobile applications. Users update at different times. At any given moment, the system serves clients running version 1.2, 1.3, and 1.4 simultaneously. If the API changes in a way that breaks older clients, users who have not updated their app get errors and the platform loses rides.
URL-Based Versioning
The system uses URL path versioning: /v1/rides, /v2/rides. This is explicit, easy to route at the API Gateway level, and visible in logs and monitoring. When a breaking change is needed (renaming a field, changing a response structure, removing an endpoint), it ships under a new version prefix.
What Counts as a Breaking Change
Breaking: removing a field from a response, renaming a field, changing a field’s type, removing an endpoint, changing the meaning of a status code.
Non-breaking (safe to add without a version bump): adding a new field to a response (old clients ignore it), adding a new optional field to a request, adding a new endpoint.
Deprecation Policy
Support at minimum the current version and one previous version (N and N-1). When v2 launches, v1 continues to work. Deprecation is announced in response headers (Deprecation: true, Sunset: 2027-03-01) so that clients can detect and adapt. After the sunset date, v1 returns 410 Gone.
Interview Tip:
Most candidates do not mention API versioning in a system design interview. Bringing it up briefly (“I would version the API at the URL level to support older mobile clients during rollout”) demonstrates production awareness without consuming interview time. One sentence is enough.
SLIDE 123: Worked Example, Design the Ride Request API End to End
Tracing POST /v1/rides Through the System

Step 1: Client sends request
The rider app sends POST /v1/rides with pickup, dropoff, and vehicle type. The request includes a JWT in the Authorization header.
Step 2: API Gateway processing
The Gateway extracts the JWT, calls the Auth Service to validate it (or validates it locally if using a shared signing key). Checks the rate limiter for this user and endpoint. If auth fails, returns 401. If rate limited, returns 429 with retry_after_seconds.
Step 3: Request validation
The Gateway (or Trip Service) validates the request body: are pickup and dropoff coordinates valid? Is vehicle_type a recognized value? Is the pickup location within a served city? If validation fails, returns 400 with a specific error code.
// 400 Bad Request
{ "error": "invalid_pickup", "message": "Pickup location is outside the service area." }
Step 4: Trip creation and matching
Trip Service creates the trip record in PostgreSQL (status: requested). Calls Matching Service, which queries the spatial index, filters and scores candidates, and returns the top match. Dispatch Service sends the offer to the driver via WebSocket.
Step 5: Response to rider
The rider receives a 201 Created with the ride_id and estimated fare. The rider app begins polling GET /v1/rides/{id} or opens a WebSocket connection to receive real-time status updates.
Step 6: Error cases
No drivers available: Matching Service finds no eligible candidates within radius. Trip transitions to “failed.” API returns 404 with error code “no_drivers_available.”
Active ride already exists: Trip Service checks if this rider already has a trip in an active state. If yes, returns 409 with the existing ride_id.
SLIDE 124: Wrong vs Right, API Design Mistakes
Common Mistake 1: REST Endpoints for Real-Time Data
Wrong: the rider polls GET /v1/rides/{id}/driver-location every 2 seconds to track the driver on the map.
At 2 million active rides, each polling every 2 seconds, the system handles 1 million HTTP requests per second just for location tracking. Each request carries full HTTP overhead: headers, TLS, connection management. This is wasteful and adds unnecessary latency.
Better: “Driver location during an active ride is streamed via WebSocket. The server pushes position updates to the rider whenever the driver’s position changes, with no client-side polling. REST is used for actions like requesting and cancelling rides. WebSocket is used for continuous data streams.”
Common Mistake 2: Offset Pagination
Wrong: GET /v1/rides/history?page=3&limit=20
With 20 million new trips per day, offset pagination produces duplicate or skipped results as the dataset shifts between page loads.
Better: “I use cursor-based pagination where the cursor encodes the last seen record’s timestamp and ID. The next page is always consistent regardless of new insertions.”
Common Mistake 3: No Error Response Design
Wrong: returning generic 500 errors or unstructured error messages for every failure case.
Better: “Each error response includes an HTTP status code, a machine-readable error code, and a human-readable message. 404 with ‘no_drivers_available,’ 409 with ‘active_ride_exists,’ 429 with ‘rate_limited’ and a retry_after_seconds field. The client can programmatically handle each case.”
Common Mistake 4: Ignoring Authentication and Rate Limiting
Wrong: designing all the service endpoints but never mentioning how they are protected.
Better: “All requests are authenticated via JWT at the API Gateway. Rate limiting is per-user, per-endpoint, using a sliding window counter in Redis. Unauthenticated and rate-limited requests are rejected at the edge before reaching backend services.”
SLIDE 125: How to Present API Design Concisely
2-3 Minutes, Three Moves
API design in an interview is a supporting section, not a deep dive. You are demonstrating that your architecture has a usable interface, not designing a full API specification.
Move 1: List 3-4 key endpoints (30 seconds)
Pick the most important endpoints and state them with HTTP method and path. Do not enumerate every endpoint. Focus on the ones that exercise interesting design decisions.
“POST /v1/rides to request a ride. GET /v1/rides/{id} for status. DELETE /v1/rides/{id} to cancel. The driver receives dispatch offers via WebSocket push, not REST.”
Move 2: Show one request/response example (1 minute)
Pick the most architecturally interesting endpoint (usually the ride request) and show the request body, response body, and one error case. This demonstrates that you think about the data contract, not just the URL.
Move 3: Mention auth, rate limiting, and pagination (30 seconds)
“Authentication is JWT-based, validated at the API Gateway. Rate limiting is per-user, per-endpoint. Trip history uses cursor-based pagination because offset breaks with real-time data insertion.”
These three sentences cover three production concerns that most candidates skip entirely. Each takes 10 seconds to say and shows significant depth.
What Not to Spend Time On
Do not enumerate every field in every request and response. Do not design the admin API. Do not discuss HTTP header conventions in detail. These are real engineering concerns but they do not add value in an interview. Spend the time on architecture, data flow, and deep-dive sections instead.
SLIDE 126: Interview Practice, API Design
Practice Exercise: Present the API Layer in 2 Minutes
Set a timer. Cover the following from memory:
- Name 3-4 rider and driver endpoints with HTTP methods (30 seconds)
- Show the ride request (POST /v1/rides) with request body, response body, and one error response (1 minute)
- State the WebSocket vs REST split, and mention auth + rate limiting + cursor pagination (30 seconds)
Evaluation Criteria
Strong answer: clear REST endpoints with correct HTTP methods. Distinguishes REST (actions) from WebSocket (real-time updates). Shows a concrete request/response example with error handling. Mentions JWT auth, rate limiting, and cursor pagination.
Adequate answer: names the key endpoints but mixes up HTTP methods or conflates REST with WebSocket use cases. Shows a request example but without error responses.
Weak answer: “The rider sends a request and gets a response.” No endpoint paths, no HTTP methods, no data contract.
Transition to Next Section
“The APIs are defined. Now let me walk through how dynamic pricing works, because the fare estimate in the ride request response depends on a real-time surge multiplier that is computed per zone.” That is Section 10: Dynamic Pricing.
SLIDE 127: Section Cheat Sheet
Key Endpoints
Rider:
POST /v1/rides Request a ride
GET /v1/rides/{id} Ride status
DELETE /v1/rides/{id} Cancel
GET /v1/rides/history Trip history (cursor pagination)
Driver:
POST /v1/drivers/status Go online/offline
POST /v1/dispatch/{id}/accept Accept offer
POST /v1/trips/{id}/complete Complete trip
WebSocket: dispatch_offer, ride_cancelled, navigation_update
Design Decisions
REST: actions (request, cancel, rate, complete)
WebSocket: continuous data (driver location, ETA updates, dispatch offers)
Push: background alerts (driver matched, trip complete, receipt)
Auth: JWT at API Gateway, refresh tokens for long driver sessions
Rate limit: per-user per-endpoint, sliding window in Redis
Pagination: cursor-based (not offset), encoded as opaque base64 string
Versioning: URL path (/v1/, /v2/), support N and N-1
Error Response Pattern
{
"error": "machine_readable_code",
"message": "Human-readable description.",
"retry_after_seconds": 30
}
How This Connects to Other Sections
Section 4 (Architecture) defined the API Gateway and WebSocket Gateway that process these requests. Section 5 (Data Flow) traced the ride request through the services these APIs call. Section 8 (Database Design) defined the schemas and indexes these endpoints read from and write to. Section 10 (Dynamic Pricing) computes the surge multiplier that appears in the fare estimate returned by POST /v1/rides. Section 12 (Caching) determines the TTLs for cached responses like ETA and fare estimates.