Split for Pressure You Can Name Not Scale You Imagine
What Uber's 2013 dispatch crisis teaches Go teams about when to split and when to wait
Split for Pressure You Can Name Not Scale You Imagine
What Uber’s 2013 dispatch crisis teaches Go teams about when to split and when to wait

1. The Real Question Is Not Monolith Or Microservices
A few years ago, I would hear teams debate microservices as if the choice were mostly technical. Should we split the database? Should every service own its API? Should we use gRPC or HTTP? Those questions matter, but they are not the starting point.
The starting point is much less elegant — what is the system preventing the organization from doing?
The trigger for this article is a long interview with Thuan Pham, Uber’s former CTO, published in May 2026. The story has plenty of dramatic details — a 2013 company with around 40 engineers, production incidents several times a week, a core dispatch path with only a few months of capacity left, a marathon interview with Travis Kalanick, and later a service ecosystem counted in the thousands. But the useful engineering lesson is not “microservices are good.”
The useful lesson is narrower — microservices are an expensive way to buy parallel work under growth pressure.
If a monolith lets ten people ship cleanly, keep it. If it lets fifty people ship with some discipline, still keep it. If hundreds of engineers are blocked on the same deploy train, same ownership model, same review queue, same runtime blast radius, then service boundaries become an organizational tool, not just a code layout.
This is where Go enters the discussion. Go is not magic for distributed systems. It will not fix bad ownership, missing observability, or weak API contracts. But Go does make a certain style of production service boring in the best way — small binaries, explicit concurrency, cheap goroutines, fast builds, readable code, and a standard library strong enough to keep teams from inventing too much.
For a fast-growing system, boring matters.
2. The Capacity Wall Arrives Before The Rewrite Plan
The most uncomfortable part of scaling is timing. Systems rarely fail after a neat architecture review. They fail while product keeps growing.
In Uber’s 2013 phase, the core dispatch path had a hard scaling problem. A single-threaded Node.js service could move to faster CPUs for a while, but vertical scaling always has a ceiling. The practical question was simple — what happens when the largest city cannot fit on one machine anymore?
I like this question because it removes taste from the discussion. You can prefer monoliths. You can prefer simplicity. I often do. But if one city requires more compute than one host can provide, your architecture must support partitioning. You need a way for multiple machines to serve one city, and you also need efficient packing so one machine can serve multiple smaller cities.
In Go, the shape of the problem might start with a dispatcher interface like this:
[embed]
The interface is intentionally boring. The dangerous part is hidden in state ownership. If Assign mutates an in-memory map on one process, you have a vertical scaling design. If Assign routes by CityID to a shard with explicit ownership, you can add machines.
A simple routing layer might look like this:
[embed]
This code is not the architecture. It is a reminder — the first real boundary is often a data and routing boundary, not a repository boundary.
Before a team says “microservices,” I want them to answer four questions:
What key partitions load? City, tenant, region, account, project, queue, or something else?
What state must stay strongly consistent? Dispatch, billing, inventory, payments, and safety flows have different tolerance for delay.
What failure can the product absorb? A delayed receipt and a wrong driver assignment are not equal.
Who owns each boundary at 2 a.m.? If ownership is unclear, the boundary is theater.
None of these four answers are really technical. They are organizational commitments dressed up in architecture language. A team that cannot answer them clearly will redraw the boundary every quarter, and every redraw costs someone a weekend. The system can survive a fuzzy interface much better than it can survive a fuzzy owner.
3. Why A Giant Monolith Becomes A Social Bottleneck
The second scaling wall is not CPU. It is coordination.
A large API monolith can start as a strength. One deployable. One shared model. One place to debug. One test suite. Early in a company’s life, those traits are valuable because every product decision crosses many domains.
Then the traffic curve and hiring curve bend upward. A payments change waits for marketplace code review. A growth experiment touches core trip code. A driver feature needs a deploy shared with unrelated rider work. The problem is no longer only runtime scale. It is the cost of unrelated teams changing the same artifact.
I once worked on a Go monolith for a fintech with around thirty-five engineers in one repository. Builds were fast, tests were fast, deploys were boring — exactly the state most monolith advocates promise. A year later, the team had grown past sixty and the same deploy train was carrying eight unrelated features at once. A small risk-engine change shipped alongside three migrations from teams I had never met, and the on-call engineer rolling them back at 2 a.m. could not name a single author. The runtime was healthy. The boundary was social, and nothing in the codebase was helping us draw it.
This is where the “thousands of services” story is easy to mock and hard to understand. No experienced engineer wakes up wanting thousands of services. Each service brings a tax — monitoring, deployment, ownership, versioning, incident response, schema migration, authentication, network failure, retries, and cost allocation.
But a blocked organization also pays tax. It pays with missed market windows, slow experiments, large risky deploys, and engineers afraid to touch shared code.
At Uber scale, the choice became less about aesthetic architecture and more about replacing one bottleneck with many smaller contracts. A new feature could become a service. Old monolith domains could be extracted over time. This did not make the system simpler. It made independent motion possible.
There is a key distinction here — microservices reduce local coordination by increasing system-wide complexity.

If leadership celebrates the first half and ignores the second, the architecture becomes debt with a logo.
Go helps only if we keep service contracts plain. For example, a handler can enforce deadlines and cancellation at the edge:
[embed]
The important line is not the JSON decoder. It is the timeout. Service-oriented systems fail by waiting too long, retrying too broadly, and hiding partial failure until users feel it. In Go, context.Context gives us a shared language for cancellation. Use it everywhere crossing a service boundary.
4. A Go Checklist For Service Splits
When I review a proposed service split, I do not start with Kubernetes YAML. I start with the consequence of owning a boundary.
Here is the checklist I use.
One owner — The service must have a team willing to own code, data, alerts, dashboards, and on-call behavior.
One reason to exist — “The file is too large” is not enough. Good reasons include separate scaling profile, separate compliance boundary, separate deploy cadence, or separate domain ownership.
One data contract — The service should own its write model. Shared database tables across services create distributed code with monolith coupling.
A rollback story — If the new service fails, can we degrade, shadow, bypass, or roll back without corrupting state?
A latency budget — Every network hop spends time. If a request path calls eight services, nobody gets to pretend p99 latency is mysterious.
Observable from day one — Logs, metrics, traces, saturation, error budgets, and dependency health must exist before launch.
In Go, I like making dependency calls visibly bounded:
[embed]
This pattern looks modest, but it encodes a serious production rule — every dependency receives a budget, not a blank check.
Large microservice systems need this discipline because they accumulate hidden queues. A slow downstream call becomes goroutines waiting. Waiting becomes memory pressure. Memory pressure becomes GC work. GC work becomes latency. Latency becomes retries. Retries become more traffic. The original bug may be small, but the feedback loop is not.
5. The China Launch Lesson — Product Pressure Changes The Math
One of the strongest time anchors in Uber’s scaling story is the 2014 push into China. The ask was extreme — build a physically separated local platform in months, while the business wanted speed across large cities. The team estimate was far longer than leadership wanted. The eventual strategy used staged rollout, with a high-volume city early enough to prove the path under pressure.
I do not read this as a simple “move faster” lesson. Most companies should not copy that operating mode. It burns people, compresses design space, and creates debt. But it explains why architecture choices can look irrational from the outside.
When the market window is narrow, delay has a price. When reliability is poor, delay also has a price. Engineering leadership must choose which price the company can pay.
This is the product trade-off I think many architecture debates miss — technical correctness is not independent from timing. A perfect platform six months late may be less correct than a limited platform shipped safely in phases. The reverse can also be true. A rushed platform in payments, medical, aviation, or safety-critical workflows can be irresponsible.
The practical move is staged risk:
- Pick the hardest slice early enough to expose false assumptions.
- Launch with limited blast radius.
- Instrument the slice before broad rollout.
- Keep rollback boring.
- Do not add unrelated features during the migration.
This is where Go’s simplicity helps again. Small services with explicit flags, config, and health checks are easier to stage. But the language is only the tool. The engineering habit is the advantage.
6. Where The Microservice Thesis Breaks
Now the counterpoint.
Many teams adopt microservices too early. They confuse future scale with current pain. They split services before they have stable domains, then spend months maintaining APIs between concepts still changing every week. They add queues because queues feel scalable. They add service discovery, tracing, mesh policy, deployment pipelines, and ownership charts before the product has found its shape.
This is how teams get distributed systems pain without distributed systems upside.
A modular monolith is often the better intermediate step. In Go, this might mean one deployable with strict package boundaries:
[embed]
Use interfaces at domain edges. Keep database access behind packages. Run package-level tests. Prevent import cycles. Measure hot paths. Only extract a service when the boundary has proven stable and the operational benefit exceeds the tax.
My rule of thumb — split for pressure you can name, not scale you imagine.
If the pressure is deploy contention, separate deployability may help. If the pressure is CPU saturation, sharding may help before microservices. If the pressure is unclear ownership, moving code to another process may make it worse. If the pressure is database contention, splitting stateless APIs will not fix the core problem.
7. AI Changes Speed, Not Accountability
The interview also touched on AI-assisted development and agent-style workflows. I am optimistic here, especially for Go. AI can write handlers, tests, migrations, dashboards, and client wrappers quickly. Strong engineers can use it to move faster because they know what to reject.
But AI also makes service sprawl easier. A team can generate a new service skeleton in minutes. That does not mean they have an owner, SLO, rollback plan, data model, or incident path.
In the AI era, the senior engineering skill becomes sharper, not softer — knowing which code should not exist yet.
For Go teams, I expect the winning pattern to be boring foundations plus faster iteration: generated clients, consistent middleware, standard observability, clear service templates, and small domain-focused code. AI can accelerate the typing. It cannot remove the need for architectural judgment.
8. The Takeaway
Uber’s 2013 to 2016 scaling story is not a clean architecture case study. It is a reminder from a high-pressure environment. A company with a fast-growing marketplace, failing systems, urgent global expansion, and hundreds of engineers had to trade simplicity for parallel motion.
That trade can be valid. It can also be disastrous when copied without the same constraints.
For my own Go systems, I keep the conclusion simple — start with the smallest architecture capable of honest ownership, clear scaling boundaries, and reliable operations. When growth makes the monolith a coordination bottleneck, split deliberately. When service count rises, invest just as aggressively in observability, contracts, and operational discipline.
Microservices are not the destination. They are one tool for buying speed when the organization has outgrown a single change surface.
References
메타데이터
- post_id
- fbf38fe2fd21
- slug
- split-for-pressure-you-can-name-not-scale-you-imagine-fbf38fe2fd21
- url
- https://medium.programmerscareer.com/split-for-pressure-you-can-name-not-scale-you-imagine-fbf38fe2fd21
- canonical_url
- https://medium.programmerscareer.com/split-for-pressure-you-can-name-not-scale-you-imagine-fbf38fe2fd21
- author_url
- https://medium.com/@wesley-wei
- status
- ok
- fetched_at
- 2026-06-14 11:28:49