113 Lessons About Monoliths and What They Actually Tell You About Engineering Knowledge
Source: lobsters
The post from semicolonandsons.com on scaling a monolith to 1M LOC is worth reading, but the format is as interesting as the content. One hundred and thirteen numbered lessons, each a paragraph or two, spanning everything from naming conventions to on-call rotations. The instinct when encountering a list that long is to look for the most quotable few items and move on. That instinct is wrong, and resisting it reveals something about how engineering expertise accumulates.
The List Is the Argument
Numbered lesson lists in software engineering usually serve one of two purposes: either they are marketing documents dressed as wisdom, or they are the genuine residue of earned experience that resists compression into a single thesis. The semicolonandsons post is the second kind. The 113-item structure is not an aesthetic choice; it is a symptom of the actual shape of the knowledge.
Software engineering wisdom does not reduce cleanly. When you have run a monolith from 10k to 1M lines of code, you accumulate roughly 113 distinct things that bit you or saved you, each contextual enough that it cannot be derived from the others. Lesson 47 about keeping database migrations backward-compatible is not redundant with lesson 71 about module boundary enforcement. They address different failure modes at different timescales, and neither is a consequence of the other.
This is what distinguishes engineering knowledge from scientific knowledge. In physics, knowing Newton’s laws plus initial conditions lets you derive trajectories. In software, knowing that large codebases need good module boundaries does not tell you that your Postgres migration will block writes on a table with 200 million rows at 3am unless you use CREATE INDEX CONCURRENTLY. Those are separate facts, both true, neither derivable from the other.
The implication for how you read posts like this one is that you should not be looking for the thesis. There is not one. You should be looking for the specific items that match the failure modes you have not hit yet, because those are the ones that will cost you.
What Shopify, GitHub, and Stack Overflow Actually Did
The “monolith versus microservices” framing that accompanies every article like this one presents a false binary, and the companies most cited in that debate spent years demonstrating that the real question is different.
Shopify’s core Rails monolith grew to the point where a single deploy touched hundreds of thousands of lines of Ruby. Their answer was not to decompose the monolith into services; it was to enforce internal component boundaries with a custom abstraction called Packwerk, which statically analyzes Ruby constant references to enforce that components do not reach across architectural boundaries at the module level. The monolith stayed a monolith in the deployment sense; it became a set of well-bounded components in the structural sense. Packwerk runs as part of CI, which means the boundary enforcement is not a convention that erodes under deadline pressure but a constraint the toolchain knows about.
GitHub ran a Rails monolith that became famous for its scale, processing millions of Git operations per day. Their primary scaling techniques were database read replicas routed by a query router, careful background job queue management via Resque and then Sidekiq, and aggressive feature flag infrastructure to decouple deploy from release. The monolith did not need to be split to handle the load; it needed operational and architectural discipline applied within the existing structure.
Stack Overflow is the canonical case, running on a handful of servers what would require hundreds at many other companies. The relevant architectural fact is not that they avoided microservices; it is that they invested heavily in two things that microservices advocates often underprioritize: caching coherence through Redis and MSSQL query plan optimization, and profiling-driven optimization rather than speculative decomposition. Nick Craver’s documented architecture is a case study in the leverage you get from knowing your system deeply rather than distributing it broadly.
None of these companies reached 1M lines of code and decided their monolith was simply fine. They each encountered real inflection points and made specific engineering investments to navigate them.
The Technical Inflection Points That Matter
Around 100k Lines
At 100k lines, the first real pain is usually test suite time. A well-maintained Rails app or TypeScript service at this size can have a test suite that takes 8 to 15 minutes in CI, which is long enough to create context-switching overhead but short enough that nobody wants to invest in parallelization. The right move at this size is to treat test time as a first-class metric before it becomes a crisis. Jest’s --shard flag and Vitest’s pool configuration for parallel workers are both available and under-used at this scale. The debt of a slow test suite compounds; sharding a 12-minute suite into four 3-minute shards costs one afternoon and pays back every day thereafter.
Database schema management also becomes a real engineering concern at 100k lines. The casual approach of writing migrations and running them is fine until you have a table with tens of millions of rows, at which point ALTER TABLE ADD COLUMN with a non-null default will hold an exclusive lock for minutes. Tools like strong_migrations for Rails and pg-migrate for Node catch these patterns statically. The time to install migration linting is before you have a production table large enough to make unsafe migrations painful.
Around 500k Lines
At 500k lines, the dominant failure mode shifts from individual technical problems to coordination overhead between people modifying the same conceptual areas without realizing it. This is where module boundary enforcement pays off. Without it, the codebase develops what the semicolonandsons post calls “load-bearing dirt”: code that everything depends on but nobody owns, that cannot be changed safely because its consumers are spread throughout the codebase.
The concrete tool investment at this size is static dependency analysis. For TypeScript projects, dependency-cruiser can enforce that certain modules do not import from certain others, expressed as rules in a config file that runs in CI. For Ruby, Packwerk does the same job. The goal is not to prevent all cross-cutting concerns; it is to make cross-cutting concerns visible and intentional rather than accidental.
Deployment complexity also starts to matter differently at 500k lines. A deploy at 100k lines is risky in the sense that any deploy is risky. At 500k lines, if you do not have feature flags, every deploy is a binary risk event: either everything merged in the last sprint is live or none of it is. LaunchDarkly and Unleash are both mature options here. The investment in flag infrastructure pays for itself the first time you need to roll back one of three features that shipped in the same deploy window without rolling back the other two.
Around 1M Lines
At 1M lines, the problem is no longer primarily technical; it is organizational. The codebase is large enough that no individual has a complete mental model of it. Changes in one area produce unexpected effects in areas that feel unrelated. This is the inflection point where the semicolonandsons list most densely concentrates its wisdom, because the failure modes at this scale are all about coordination, ownership, and the slow accumulation of implicit contracts between modules.
Two specific technical investments matter here. First, your CI pipeline should have a dependency graph, meaning it knows which tests are affected by which files and can run the minimal test set for a given change rather than the full suite. Nx for monorepos and Turborepo both provide affected-file analysis. Without this, the full test suite at 1M lines is probably 30 to 60 minutes, which is long enough to functionally prevent the rapid iteration that keeps a codebase healthy.
Second, database schema management at 1M lines requires an explicitly owned migration process. The pattern of individual developers writing migrations without review is fine at 100k lines because the blast radius of a mistake is bounded. At 1M lines, with dozens of engineers and a schema with hundreds of tables, a migration that adds an index without CONCURRENTLY, or that adds a foreign key constraint that locks both tables during backfill, can take down production for minutes. Most teams that have not explicitly solved this problem solve it the hard way.
The Philosophical Tension That Is Not Actually a Tension
The microservices-versus-monolith debate has been conducted mostly at the wrong level of abstraction. The question is not which architectural style is correct; it is what problems you are actually trying to solve and whether those problems have better solutions than splitting the deployment unit.
The real costs of microservices that advocates understate are the ones that only appear under operational pressure: distributed tracing becomes non-optional when a request spans eight services, which means investing in something like Jaeger or Grafana Tempo before you need it rather than after. Integration testing becomes significantly harder when each service has its own deployment lifecycle. The contract between services becomes load-bearing, which means versioning your API contracts explicitly with something like Pact or accepting that changes to shared interfaces are permanently expensive.
None of these costs make microservices wrong for every situation. But the decision to extract a service should be driven by a specific, identified problem, not by a general sense that the monolith has gotten large. The specific problems that services legitimately solve are: independent scaling requirements, where one component has a fundamentally different resource profile than the rest; independent deployment requirements, where one team needs to ship without coordinating with others; and language or runtime requirements, where the right tool for a job genuinely cannot live in the monolith’s runtime.
“The monolith is getting large” is not on that list. A 1M-line codebase with good module boundaries, a fast CI pipeline, automated migration linting, and feature flag infrastructure is operationally tractable. The work to get there is real, and the semicolonandsons post is a map of most of it. The 113 lessons are not redundant with each other because the monolith scaling problem does not reduce to fewer than roughly 113 distinct things you have to get right.