InterviewKata
Software architecture · Case study 08 · Structure and evolution

Shopify: the modular monolith

Shopify's core is one of the largest Ruby on Rails applications in existence: over 2.8 million lines of Ruby, worked on by more than a thousand developers. When the monolith's downsides started to outweigh its benefits, Shopify did not split it into microservices. It kept one codebase and one deployment, reorganised the code by business domain, and built tooling that rejects a pull request when one component reaches into another's internals. Only two pieces were split out, each for a reason that can be stated in one sentence.

Real caseAnalysed from two Shopify Engineering posts: "Deconstructing the Monolith" (Kirsten Westeinde, February 2019) and "Under Deconstruction: The State of Shopify's Monolith" (Philip Müller, September 2020).

1. The situation

Shopify grew for over a decade as a single Rails monolith: billing merchants, third-party apps, products, checkout, shipping, all in one codebase with no boundaries between them. The code that calculated shipping rates lived alongside checkout code, "and there was very little stopping them from calling each other". The result was extremely high coupling: changes in one place broke things elsewhere, and developers needed context on the whole system to change any part of it.

Before choosing a remedy, the team surveyed every developer working in the core system about their main pain points, "to be data-informed", and in early 2017 a small team started the work.

2. What mattered

3. The options

OptionGoodBad
Keep the monolith as it isNo migrationCoupling keeps growing with every feature and developer
MicroservicesHard boundaries enforced by the networkThe team's view: "microservices would bring their own set of challenges": distributed systems, data ownership, a long and risky migration of a decade of code
Modular monolithBoundaries by domain inside one codebase and one deployment; fix the coupling without distributing itBoundaries are only as strong as the tooling that enforces them

4. The decision

Decision: Evolve the core into a modular monolith ("Componentization")

Context: One Rails codebase, 10+ years, 1,000+ developers, no internal
  boundaries; a developer survey named coupling and lack of ownership as the
  main pain points.
Decision: Keep one codebase and one deployment. Reorganise code by business
  domain (orders, shipping, inventory, billing) instead of by Rails concept
  (models, views, controllers). Each component exposes a public API and
  exclusively owns its data. Enforce boundaries with tooling in CI.
  Extract a separate service only for a specific reason.
Consequences:
  + Developers work in what feels like a smaller app
  + No network between business domains; one deploy, one test suite
  - Boundaries need tooling and a cultural shift; progress is gradual
  - A dense, cyclic dependency graph must be untangled component by component
Compliance: dependency and privacy violations rejected in pull requests

5. How it works

From folders to enforced boundaries, in the order Shopify did it.

6. The numbers

7. What happened

Eighteen months after the first post, Shopify reported that it stood by the original ideas but that "almost all of the details have changed". The biggest lesson was that public interfaces alone didn't help: when the dependency graph was ignored, a public interface "turned out to just be an added layer of indirection", spreading a design problem into more files. The work shifted to the dependency graph: removing cycles and reducing how much depends on central entities like Shop. Discussions about which way a dependency should point regularly exposed objects with too many responsibilities and missing abstractions. Packwerk was later released as open source.

Shopify kept splitting rare and deliberate. Two examples from the 2020 post: storefront rendering became a separate service because it is a read-only, very high-throughput use case that makes sense to scale and distribute apart from the merchant admin; and credit card vaulting is separate because it processes sensitive data that shouldn't flow through the rest of the system.

8. Lessons

9. Your turn

  1. Label the classes (15 min). Take 30 files from a codebase you know and label each with a business component. Which files don't fit anywhere?
  2. Draw the graph (10 min). Draw dependencies between those components. Where are the cycles, and what does everything depend on?
  3. Justify one split (5 min). Name one part you would extract as a service, in one sentence about a characteristic that differs.
Say it in the interview
  • "Our monolith is a mess. Microservices?" First modularise it by domain inside the codebase and enforce the boundaries in CI, as Shopify did. Then extract a service only where a characteristic genuinely differs: very different scaling, or sensitive data that must be isolated.

10. Check yourself

Why did Packwerk replace Wedge?

Wedge traced calls through the whole test suite in over an hour; Packwerk's static analysis takes minutes.

What did Shopify say it learned in its 2020 post?

Cycles and central dependencies kept components coupled; the work moved to the graph.

Why is storefront rendering a separate service?

A different scaling characteristic justified a separate quantum.

11. Sources and further reading

SourceWhy
Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity (2019)The decision, the reorganisation by domain, Wedge.
Under Deconstruction: The State of Shopify's Monolith (2020)Packwerk, the dependency graph, the extracted services.
Packwerk (GitHub)The open-source boundary checker.