InterviewKata
Software architecture · Case study 07 · Structure and evolution

Segment: back from microservices

Segment, a customer data platform, forwards every event its customers send to the tools they use: analytics, advertising, email, more than 140 "destinations". To stop one failing destination delaying all the others, the team gave each destination its own queue and service. It worked, then it became the problem: 140 services, shared libraries in a dozen versions, autoscaling tuned by hand, and engineers paged at night for load spikes. In 2018 they merged the destinations back into one service, and wrote one of the most candid posts about what that cost them.

Real caseAnalysed from "Goodbye Microservices: From 100s of problem children to 1 superstar" (Alexandra Noonan, Segment engineering blog, 2018; now on Twilio's blog).

1. The situation

Segment's API receives events (a page view, a purchase) and sends each one to every destination the customer has switched on, translating it into that destination's format. Some destinations handle a handful of events a day; others thousands a second. Failures are routine: partners time out, rate-limit or go down. Retry-able errors (timeouts, 500s, rate limits) are retried later.

2. What mattered

3. The path, in three architectures

ArchitectureWhat it fixedWhat it broke
One queue, one serviceSimpleHead-of-line blocking: a failing destination's retries flooded the shared queue and delayed every destination
A queue and a service per destination, each in its own repositoryIsolation: only the failing destination's queue backed up; failing tests no longer blocked other destinationsShared libraries diverged into many versions; 140+ services to deploy, scale and watch; operational overhead grew linearly with each destination
One service, one repository, a new delivery system (Centrifuge)One version of every dependency; one deploy; a mixed worker pool that absorbs spikesFault isolation inside the process; less effective in-memory caching; a dependency upgrade can break several destinations

4. The decision

Decision: Merge all server-side destinations into one service and one repo

Context: 140+ destination services, each with its own queue, repo and
  autoscaling; shared libraries diverged across them; on-call paged for load
  spikes on low-traffic destinations; a shared-library change meant testing
  and deploying dozens of services.
Decision: Replace per-destination queues with Centrifuge, which sends events
  to one destination service. Move all destination code into one repository
  with one version of each of the 120 dependencies. Record and replay partner
  HTTP traffic in tests (Traffic Recorder) so the suite is fast and reliable.
Consequences:
  + One deploy for a shared-library change; a large worker pool absorbs spikes
  + Tests for all destinations run in milliseconds instead of up to an hour
  - A crash bug in one destination can take down delivery for all
  - In-memory caches are spread over 3,000+ processes, so hit rates fall
  - Upgrading a dependency may break several destinations at once
Kept: isolation of failing endpoints, now inside Centrifuge (queues per
  customer and destination, retries and archiving)

5. How it works

One destination, X, starts timing out. Watch what each architecture does with its events.

6. The numbers

7. What happened

The post reports that developer productivity "substantially improved": one engineer can deploy the service in minutes, and the team no longer gets paged for small destinations' load spikes. It is equally clear about the costs, listed as trade-offs in the decision above, and that the team was working on a way to stop one destination crashing the whole service while keeping the monolith. Centrifuge, the system built to make the merge possible, later became the infrastructure behind Segment's Connections product.

The authors' own conclusion is precise: microservices "work well" in some parts of their infrastructure; the server-side destinations, where every service did nearly the same job with the same libraries, were the wrong place for them.

8. Lessons

9. Your turn

  1. Count the cost per service (10 min). For a system you know: how many deploys, dashboards, alerts and scaling configs does each service add?
  2. Find the shared library (10 min). Which library do most of your services depend on? How many versions of it are running today?
  3. Redesign the isolation (10 min). Where else could Segment have isolated a failing destination without a service per destination?
Say it in the interview
  • "Segment went back to a monolith. Why?" They had split 140 near-identical destinations into services to isolate failures. The isolation they needed was in the queue; the services added deploys, scaling configs and diverging shared libraries for every destination, so they merged the code and kept the isolation in the delivery system.

10. Check yourself

What problem did the original move to a queue and service per destination solve?

In one shared queue, a failing destination's retries flooded the queue for everyone.

What made moving all destinations into one repository safe?

Traffic Recorder replays recorded responses, so tests became fast and reliable.

Which trade-off did Segment accept with the monolith?

Fault isolation inside one process is harder; they listed it openly.

11. Sources and further reading

SourceWhy
Goodbye Microservices: From 100s of problem children to 1 superstarThe original post: every fact on this page.
Centrifuge: a reliable system for delivering billions of events per dayHow the delivery system isolates each customer and destination, retries and archives.
MonolithFirst (Martin Fowler)Why boundaries are easier to find before splitting.