Keep the monolith majestic
I Hated Microservices. Agents Love Them.
Small services with clear contracts are the perfect unit of work for a coding agent, and a monorepo means every agent can still see the whole system.
I was a Majestic Monolith guy. DHH wrote the essay and I nodded along so hard I probably pulled something.
When someone told me they were splitting their three-person startup into eleven services, I'd ask why. What problem do you have that a Rails app and a job queue can't solve? Usually the answer was some version of "Netflix does it."
Netflix has thousands of engineers. You have Kevin.
Microservices were overhead for small teams. Network calls where method calls used to be. Deploy orchestration. A request that hops through four processes and dies in the third with a log line nobody can find. A repo per service, each with its own CI and its own README that was last accurate in 2019.
For human teams, I still mostly believe that. But I'm not the one writing the code anymore, and I'm rarely running one agent. I'm running three or four at once, each in its own worktree. Whether that goes well has very little to do with the model and almost everything to do with the shape of the codebase.
Same three features, two codebases
Here's a concrete Tuesday. I'm building a podcast tool and I want three things done in parallel:
- Support private RSS feeds with basic auth.
- Split audio on silence instead of fixed 30 second windows.
- Build transcript search.
In the monolith, all three live in one Rails app. Agent 1 touches Feed, FeedFetcher, and adds a migration for credentials. Agent 2 touches Episode, Transcript, the TranscribeJob, and adds a migration for segment timestamps. Agent 3 touches Transcript too, adds a tsvector column, and a search controller.
Now watch it go sideways. Agents 1 and 2 both regenerate db/schema.rb and both bump the version line, so whoever merges second gets a conflict. Agent 2 changes how Transcript stores segments while Agent 3 is writing search against the old shape. Both lean on a Processable concern that Episode and Transcript share, and Agent 2 "cleans it up" along the way. Agent 3's branch goes red for reasons it didn't cause. The suite takes twelve minutes and runs everything, so Agent 1 sits waiting on a fixture Agent 2 broke. I spend the afternoon untangling merges, which is exactly the job I was trying to hand off.
None of those agents did anything dumb. They did what anyone does in a codebase where everything touches everything. I've done it myself, to coworkers, more than once.
In the monorepo, the same project looks like this:
podcast-tools/
├── apps/
│ └── web/ # Rails. Accounts, billing, UI, search
├── services/
│ ├── ingest/ # Go. Pulls feeds, downloads audio
│ └── transcribe/ # Python. Whisper, diarization, chunking
├── packages/
│ └── contracts/
│ ├── ingest.openapi.yml
│ └── events/
│ ├── episode.downloaded.json
│ └── transcript.ready.json
└── CLAUDE.md(Yes, three languages. That's another post.)
The same Tuesday:
Agent 1 (services/ingest): private RSS feeds with basic auth
Agent 2 (services/transcribe): split audio on silence
Agent 3 (apps/web): transcript search against transcript.ready.jsonThree directories, three test suites, three databases. Nobody shares a schema file or a concern. Agent 3 doesn't know or care how transcription works. It reads transcript.ready.json and builds against that shape. If Agent 2 needs segment timestamps in the payload, it changes the schema in packages/contracts first. That diff is ten lines, and it's the one place I actually read carefully.
The same day, side by side:
| Majestic monolith | Monorepo of small services | |
|---|---|---|
| Context per agent | The whole app, every model and concern that might matter | One service plus the contracts, a few thousand lines |
| Merge conflicts | schema.rb, shared models, shared concerns | Rare, and they land in packages/contracts where you want them |
| Test feedback | One slow suite, others' breakage blocks you | Each service runs its own suite in seconds |
| Deploy | One deploy, done | Three things to ship, version, and keep alive |
| Debugging | One stack trace | A request crossing processes, you need correlation IDs |
| Data consistency | Real transactions across everything | Events, retries, eventual consistency |
| Ops | One app, one database, one dashboard | More moving parts, full stop |
The top half is why I switched. The bottom half is why I argued against this for years, and I was right about it.
Monorepos add context, services subtract it
A focused agent with 4,000 lines in front of it does better work than the same agent wading through 400,000. That's what microservices buy you: less context. For a human team it was an abstract architecture virtue that came bundled with real operational pain. For an agent it's the whole point.
The part the old microservices crowd got wrong was the repo per service. That's what made it miserable, and for agents it's worse, because the agent in one service can't see the others. When something breaks across a boundary, say an episode never gets a transcript, I want an agent that can read the Go fetcher, the Python worker, the Rails job that kicks it off, and the contracts between them. Same repo. No cloning four things and pasting files into a chat.
Small context to work in. Big context to look at. You want both.
Dijkstra said the tools we use have a profound influence on our thinking habits. I used to quote that about languages. It's just as true of architecture. The monolith trained me to think about one person holding the whole system in their head. That person isn't the bottleneck anymore. Coordination between workers is.
Where the monolith still wins
Three services means three things to deploy and monitor. Network calls fail in ways method calls don't. When a payment and an account update need to succeed or fail together, a database transaction beats any saga you'll design. An agent writing the code doesn't change the fact that at 2am something between ingest and transcribe dropped a message, and I'm the one who gets to find it.
So I don't split everything. Billing, accounts, and anything that needs a transaction stay in the Rails app. I split along seams where the work is naturally async and the boundary is a payload, not a join.
And if you're one person with one agent building a CRUD app? Keep the monolith. None of this matters until you're running parallel work, and a Rails monolith is still a wonderful place to live.
I hated microservices because they were built around the coordination problems of big teams. Turns out I manage a big team now. It's just made of agents.
This is part of Rethink Everything. Next up: the other half of making services cheap, which is owning your own infrastructure.