Taxi Hailing Deep Dive
Session 8.8 · ~5 min read
From Monolith to 2,200 Microservices
Uber launched in 2010 as a single Python application. One codebase handled everything: rider requests, driver matching, trip tracking, payments, and email notifications. By 2014, this monolith was struggling. Deployments took hours. A bug in the notification module could bring down the entire matching system. Teams stepped on each other's code constantly.
By 2020, Uber had grown to roughly 2,200 microservices. This did not happen overnight, and it did not happen without pain. The migration followed a pattern that Martin Fowler named the Strangler Fig, after the tropical vine that grows around a host tree, gradually replacing it until the original tree is gone. The old system stays alive while new services absorb its responsibilities, one domain at a time.
Key insight: You do not rewrite a monolith. You strangle it. Each extracted service replaces one responsibility of the original system while the monolith continues serving everything else. The monolith shrinks until it disappears.
The Strangler Fig Pattern
The pattern works in three phases for each domain you extract.
Phase 1: Intercept. Place a routing layer (an API gateway or proxy) in front of the monolith. All traffic flows through this layer. Initially, it forwards everything to the monolith unchanged.
Phase 2: Extract. Build the new microservice alongside the monolith. Route a subset of requests to the new service while the monolith handles the rest. Run both in parallel. Compare outputs to verify correctness (shadow mode).
Phase 3: Retire. Once the new service handles 100% of its domain's traffic and has proven stable, remove the corresponding code from the monolith. The monolith is now smaller.
All domains] end subgraph "Phase 2: Extract" C2[Clients] --> GW2[API Gateway] GW2 -->|trips traffic| NS[New Trips Service] GW2 -->|all other traffic| M2[Monolith
Shrinking] NS -.->|shadow compare| M2 end subgraph "Phase 3: Retire" C3[Clients] --> GW3[API Gateway] GW3 --> NS2[Trips Service] GW3 --> PS[Payments Service] GW3 --> MS[Matching Service] GW3 --> M3[Monolith
Minimal] end
Repeat this for each domain. The monolith shrinks with every extraction. The gateway grows more routing rules. After many iterations, the monolith contains only legacy code that nobody wants to touch, and eventually that too gets replaced or deleted.
Domain-Driven Decomposition
The critical question is: how do you decide where to cut? Uber's answer was Domain-Oriented Microservice Architecture (DOMA), published in 2020. Instead of decomposing by technical layer (one service for the database, one for caching, one for business logic), they decomposed by business domain.
A domain is a collection of related microservices that own a business capability. Each domain has a gateway that other domains must use to interact with it. Services within a domain can call each other freely. Cross-domain calls go through the gateway.
| Domain | Services Extracted | Owns | Key Data Store |
|---|---|---|---|
| Trips | Trip lifecycle, trip state machine, trip history | Trip creation, status updates, completion | Cassandra (trip events), MySQL (trip metadata) |
| Matching | Supply positioning, demand prediction, dispatch optimization | Driver-rider pairing algorithm | In-memory geospatial index, Redis |
| Payments | Fare calculation, payment processing, invoicing, payouts | All money movement | MySQL (ledger), payment gateway integrations |
| Pricing | Surge pricing, fare estimation, upfront pricing | Price computation for all products | Real-time demand signals, ML model server |
| Maps | Routing, ETA, geocoding, map tile serving | All location and navigation data | Graph DB (road network), tile cache |
| Marketplace | Supply-demand balancing, incentives, promotions | Market health and efficiency | Real-time streaming (Kafka), analytics DB |
Each domain is owned by a team (or group of teams). The gateway enforces a contract: if the Trips domain changes its internal service structure, other domains do not need to update their code. They still call the Trips gateway with the same API. This is the key benefit. Without gateways, 2,200 services calling each other directly creates a "distributed monolith" where any change requires coordinating with dozens of teams.
Before and After
What Breaks During Extraction
Extracting a domain from a monolith is not a clean lift-and-shift. Things break in predictable ways.
Shared database. The monolith uses one database. The Trips table has foreign keys to the Users table, which has foreign keys to the Payments table. When you extract Trips into its own service with its own database, those foreign keys disappear. You replace them with API calls. A query that was a single SQL join now becomes two network round-trips. Latency increases. Consistency becomes eventual instead of transactional.
Distributed transactions. In the monolith, "create a trip and charge the rider" is one database transaction. In microservices, it spans two services and two databases. You need a saga pattern: the Trips service creates the trip, the Payments service charges the rider, and if the charge fails, the Trips service compensates by canceling the trip. This compensation logic did not exist in the monolith.
Observability gap. In the monolith, a stack trace shows the full request path. In microservices, a single user request might touch 15 services. Without distributed tracing (Jaeger, Zipkin), debugging a slow request becomes guesswork. Uber built their own tracing system to handle this.
Deployment complexity. The monolith had one deployment pipeline. Now you have hundreds. Each service has its own CI/CD, its own canary process, its own rollback procedure. Uber invested heavily in internal tooling to manage this. Without that investment, the operational overhead of microservices outweighs the benefits.
Surge Pricing: A Balancing Loop
Surge pricing is one of Uber's most visible (and controversial) features. From a systems thinking perspective, it is a textbook balancing loop with a dampener. We covered balancing loops in Session 0.5. Here is how it applies.
When demand exceeds supply (more ride requests than available drivers), the surge multiplier increases. Higher fares do two things: they reduce demand (some riders decide the trip is not worth 2.5x the price) and they increase supply (drivers see higher fares and drive toward the surge zone). Both effects push the system back toward equilibrium. When supply catches up with demand, the multiplier drops.
Without the dampener, the surge multiplier would overcorrect. Prices spike to 5x, all drivers flood the zone, supply massively exceeds demand, prices crash, drivers leave, demand spikes again. The dampener smooths the multiplier changes: instead of jumping from 1.0x to 3.0x instantly, it ramps up gradually (1.0x, 1.2x, 1.5x, 1.8x...) and ramps down the same way. This prevents the oscillation pattern that plagues undamped feedback loops.
Further Reading
- Introducing Domain-Oriented Microservice Architecture (Uber Engineering Blog, 2020)
- Strangler Fig Application (Martin Fowler)
- Explore Uber's Microservice Architecture (Edureka on Medium)
- Uber System Design: A Complete Architectural Deep Dive (Grokking System Design)
Assignment
Uber started as a monolith. You are the architect tasked with extracting three domains into independent microservices: Trips, Payments, and Matching.
For each domain:
- What data does the new service own? What tables move out of the shared database?
- What cross-domain dependencies exist? (For example, Trips needs to call Payments to charge the rider.) List the API contracts between domains.
- What breaks when you remove the shared database? Identify at least one query that was a single SQL join and is now a cross-service call. How does latency change?
- Describe one distributed transaction that now requires a saga. What is the compensation action if a step fails?
Draw a before/after diagram showing the monolith's internal modules becoming independent services with defined API boundaries.