Let's start with a simple, honest picture of how a big company like Uber builds software today. Then we'll slowly see where it breaks, and how a clever idea fixes it.
Step 1: One big program vs. many small ones
In the old days, companies often built one giant program that did everything. Payments, trips, pricing β all in one place, sharing one database.
What is a database?
A database is just an organized place to store data, like a giant spreadsheet the program reads from and writes to.
Example: a balance cell that says "Rider owes $12.50."
Around 2014, Uber moved away from the one-giant-program style. They chose microservices instead.
What is a microservice?
A microservice is a small, independent program that does one job and owns its own data. Instead of one big program, you have many little ones talking to each other.
OLD WAY (monolith) NEW WAY (microservices)
βββββββββββββββββββββ βββββββββββ βββββββββββ
β ONE BIG PROGRAM β βPayments β β Pricing β
β Payments β β + DB β β + DB β
β Pricing β βββββββββββ βββββββββββ
β Trips β βββββββββββ βββββββββββ
β Driver State β β Trips β β Driver β
β (one database) β β + DB β β State β
βββββββββββββββββββββ βββββββββββ βββββββββββ
Uber ran 1,000+ microservices. Why? Because thousands of engineers could then work on their own small piece without stepping on each other. Each team ships when ready. No giant shared program to slow everyone down.
This looked clean and sensible. Payments had its own service and database. Pricing had its own. Trips had its own. Driver state had its own. Nice boxes on the org chart.
Step 2: Where it quietly breaks β the money problem
Here's the trouble. A single trip touches many services at once. By 2016, one ride could pass through dozens of them.
When you take a trip, roughly this happens:
- Payments authorizes your card.
- Pricing calculates the fare.
- Trips records that the ride happened.
- Later, maybe the fare gets adjusted, and a payout goes to the driver.
Each of these lives in a different service with a different database. And here's the key: each one saves its own data independently.
What is a transaction?
A transaction is a way to make several changes happen all together or not at all. Like a light switch β fully on or fully off, never stuck halfway.
Example: "Take $12 from the rider AND mark the trip complete" β both, or neither.
The catch: a transaction normally only works inside one database. When your data is split across many separate databases, you can't wrap them all in one transaction.
So this can happen:
Payments DB: β
Payment succeeded ($12 charged)
Trips DB: β Still thinks the ride is "open"
Pricing DB: β
Fare adjusted to $14
Ledger: β No matching entry for the $2 change
The databases now disagree about reality. For a few seconds β or a few minutes β nobody has one clear answer to "what actually happened?"
This is called being eventually consistent.
What is eventual consistency?
It means the different databases will eventually agree... but not right now. For a while, they hold different versions of the truth.
Example: two clocks that will match after someone syncs them β but for now they show different times.
For a photo-sharing app, "eventually" is fine. For a company moving billions of dollars, "eventually" is a nightmare. When a rider disputes a fare and finance is asked "prove what happened," the answer "well, the systems disagreed for a minute" is not just a data bug β it's a legal problem.
Databases disagree. Money doesn't lie.
Step 3: The mindset flip β stop trusting the database
Here's the big idea that fixes it.
Normally, a database stores the current state. It holds one row that says "balance = $12," and when something changes, you overwrite it.
UPDATE balance SET amount = 14 WHERE account_id = 42;
The problem: once you overwrite 12 with 14, the 12 is gone forever. You can't prove what it used to be, or why it changed. The current value is all you have.
So Uber flipped the whole idea:
The database is a lie you optimize. The log is the truth you keep.
Instead of storing the answer, store every event that led to the answer β and never erase anything.
Step 4: Event sourcing, step by step
This approach is called event sourcing.
What is event sourcing?
Event sourcing means you never store just the final state. Instead you store an ever-growing list of events β the individual things that happened, in order. The current state is rebuilt from that list whenever you need it.
What is an event?
An event is a small, factual record of something that happened, at a moment in time. It's written in the past tense because it already happened.
Example: PAYMENT AUTHORIZED at 10:03:21.
Uber built a system called LedgerStore on this idea. Every money movement became an event. Picture the list of events for one trip:
PAYMENT AUTHORIZED 10:03:21
FARE CALCULATED 10:03:22
TRIP COMPLETED 10:03:45
FARE ADJUSTED 10:05:12
LEDGER ENTRY 10:05:13
PAYOUT SETTLED 10:06:02
Two rules make this powerful. The log is append-only and immutable.
What is "append-only"?
Append-only means you can only add new items to the end. You can never go back and change or delete an old one.
Like a notebook written in permanent ink β new lines only, no erasing.
What is "immutable"?
Immutable means "cannot be changed after it's written." An event, once recorded, is frozen forever.
So instead of overwriting a balance, you insert a new event:
-- The event-sourcing way: only ever ADD
INSERT INTO ledger (account_id, amount, event_id, timestamp)
VALUES (42, -12.00, 'evt_a1b2', '10:03:21');
-- NEVER this:
-- UPDATE balance SET amount = 14 WHERE account_id = 42;
Notice event_id. That tiny detail matters a lot.
What is idempotency?
Idempotency means doing the same thing twice has the same effect as doing it once. No accidental double-charges.
Example: pressing an elevator button 5 times β the elevator still comes once.
In a network, messages sometimes get sent twice by mistake. If each event carries a unique event_id, the system can say "I already recorded evt_a1b2 β ignore the duplicate." That's how the same money movement never gets counted twice.
Step 5: So where's the balance now?
If you never store the balance, how do you know what someone owes?
You compute it by replaying the events. Add up all the money movements from the beginning:
Start: $0.00
PAYMENT AUTHORIZED β -$12.00
FARE ADJUSTED β -$2.00
-----------------------------------
Current balance: -$14.00
This computed answer is called a projection.
What is a projection?
A projection is a current view built by replaying the events. It's not the truth β it's a summary of the truth, calculated on demand.
Like adding up every receipt in a shoebox to find your total spend. The receipts are the truth; the total is the projection.
THE TRUTH (kept forever) THE VIEW (rebuilt anytime)
ββββββββββββββββββββββ ββββββββββββββββββββ
β append-only log of β replay β balance = -$14 β
β every event, ever β ββββββββΊ β (a projection) β
ββββββββββββββββββββββ ββββββββββββββββββββ
The beautiful part: if a projection ever gets confused or corrupted, you just throw it away and rebuild it by replaying the events. You can never do that with an overwritten row β the old data is already gone.
One truth. Replayable.
Step 6: The honest catch β complexity didn't vanish, it moved
Here's the part most explanations skip. Event sourcing is not free magic. It doesn't remove the hard parts β it moves them somewhere else.
What got harder:
- Reads got slower and trickier. "What's the balance right now?" used to be reading one row. Now it's replaying events into a projection. And projections can drift (get out of sync) or lag (fall behind) and sometimes need rebuilding.
- Storage exploded. You keep every event forever. You never delete. Over billions of trips, that's a mountain of data.
So why do it? Uber made the trade with their eyes open:
You PAY in: You GET:
- more storage - perfect, permanent history
- harder reads - ability to prove what happened
- rebuildable state
For a company moving billions in payments, being able to prove exactly what happened is worth far more than cheap storage or fast reads. In finance, that's the right call. For a casual app, maybe it isn't. The point is to choose on purpose.
The core lessons
- Splitting data across many services breaks transactions. When each microservice owns its own database, you can't wrap changes in one all-or-nothing transaction, so systems disagree for seconds or minutes.
- "Eventually consistent" is fine for photos, dangerous for money. When you move billions, a few minutes of disagreement is a legal problem, not a data bug.
- **Event sourcing keeps the events, not the answer.** Every change becomes an append-only, immutable event. You never overwrite; you only add.
- The balance is a projection β a computed view you can rebuild by replaying the log. If the view breaks, throw it away and replay.
- Unique event IDs give you idempotency β the same money movement can't be counted twice, even if a message arrives twice.
- The complexity doesn't disappear; it moves. You pay in bigger storage and harder reads, and you buy a permanent, provable history.
- The deepest idea: the database's current state is a convenient summary you optimize for speed β but the immutable log is the real truth you keep.
