Do microservices make development harder? Yes — here’s the complexity I’d only pay for on purpose
Casey Holt
September 18, 2026
Yes. Microservices make development harder. I say that as someone who has split a monolith on purpose, inherited a mesh I did not ask for, and also kept a modular Rails app together longer than the architecture guild wanted. The question is not whether distributed systems add cost. They do. The question is which costs I am buying, and whether I can name the failure I am paying to avoid.
I have paid for independent deploys when two teams were actually blocking each other on a release train. I have also paid for independent deploys when one team wanted a résumé and a Kafka diagram. Only one of those was a good purchase. Here is the complexity I will still buy, and the kind I will undo first if I get the chance.
What got harder the last time I split the wrong thing
We carved a “billing service” out of a Django monolith because checkout was getting slow in the same deploy as the marketing CMS. That sentence already had two problems. Checkout was slow because of an N+1 on order lines, not because Django could not scale. The CMS belonged in a different module, not necessarily a different process. We did the process split anyway. Six months later, creating a customer required a saga: auth service, billing, entitlements, email. A failed step left a user who could log in and could not see their invoices. Support had a runbook. Engineering had a Slack channel named after the saga.
Local development went from docker compose up plus a seed to a script that started four services, a broker, and a set of flags that pretended Stripe was up. CI got honest and slow. We added contract tests after the third time a protobuf field renamed in one repo and silently defaulted in another. None of that is surprising. It is the brochure if you read the footnotes.
What surprised the people who had pushed for the split was not the infra. It was product velocity. A “simple” change — add a tax ID field — now touched three PRs, a migration window, and a versioned API we did not want to version. In the monolith it would have been one PR and a follow-up for the admin. We had bought isolation and spent coordination.

The complexity I will pay for on purpose
I will pay for a service boundary when the failure modes and the owners are already different.
Example I still stand behind: a PDF rendering worker in Go, fed by a queue, sitting next to a Rails app. Rendering was eating request threads. The team that cared about fonts and timeouts was not the team that cared about invoices. The contract was a job payload and an object storage URL. We could deploy the worker without touching the web app. If the worker died, the web app still took payments and showed “generating.” That is a microservice in the boring sense — a separately scalable process with a narrow interface. I would do it again.
I will pay for a separate service when the runtime must be different. A Python training job, a Rust parser, a Node thing we regret but cannot rewrite this quarter. Polyglot is a reason. “We might want polyglot later” is not.
I will pay for a separate deployable when compliance or tenancy forces a wall. A PCI-ish cardholder data zone. A customer who requires their processing in a region the rest of the app does not live in. Those walls are expensive. They are cheaper than pretending a module boundary is an audit boundary.
I will pay for independent scale when I have a measurement, not a fear. p99 on checkout, CPU on image processing, a queue depth I can show you. “What if we get famous” is not a measurement.
That is a short list. It is supposed to be short.
The complexity I treat as a tax, not a feature
A service per noun. UserService, OrderService, NotificationService because the domain model had those words. Nouns are not teams. Nouns are not SLOs. This is how you get a distributed Active Record with extra latency.
A shared database across “services.” If they share the tables, they are modules with a network hop. I have inherited this. I would collapse it. The hop is not a boundary. It is a way to fail in the join you used to do in SQL.
A mesh, a service catalog, and twelve-factor purity before you have three services that need them. Envoy and Istio are real tools. They are not a substitute for a product. I have seen a three-service shop run a platform team of four. The platform was the product. The customers were an afterthought.
Synchronous chains that are just function calls with JSON. If POST /checkout must wait for inventory, tax, payments, and email, you did not decouple. You added timeouts. Use a database transaction in one process, or an explicit workflow (Temporal, a state table, a job chain you can see). Do not invent a distributed transaction with retries and hope.
Event streaming as the default integration. Kafka is excellent when you need a durable log many consumers will read at their own pace. It is a very expensive Unix pipe when two services need to agree on one checkout. I like Postgres as the first bus: a table, a worker, a metric. I graduate to a broker when a second consumer is real, not hypothetical.
A modular monolith is not a consolation prize
Most of the systems I am proud of look like one deployable with boring module seams. In Rails: packages or engines, Billing::, Identity::, no cross-import except through a small public API. In Java: Gradle modules, no cycle, Spring configs that do not become a junk drawer. In Go: a single binary with internal packages and a clear cmd/. You can still have multiple processes later. You cannot easily glue a badly split brain back together without a year of dual writes.
The test I use: can a new engineer find the code that takes money without opening four repos? If no, I have already made development harder. Sometimes that is worth it. Often it is a museum of good intentions.
I still write contract tests at module boundaries inside a monolith when the temptation to “just import the other package” shows up. The point of a boundary is the temptation. If there is no temptation, you did not need the wall.

People, not diagrams, are the usual real reason
The honest reason many companies split is org design. Conway’s law is not a suggestion. If you have two teams that cannot share a repo without a merge war, you will get two deployables whether the domain wants them or not. I have done that split. I document it as an org split, not as a technical vision. Then I invest in the contract — OpenAPI or a proto, a staging environment that is not a rumor, an owner on each side of the call.
If you have one team of six and eight services, you have made a hobby of YAML. I will not romanticize that as “we are a platform company.” You are a small team with a large operational surface. Hire, or collapse, or accept that every feature is a distributed systems project. I have accepted that once, for a marketplace with real isolation needs. I have refused it for a SaaS with 12 engineers who wanted to look like Netflix.
On-call gets harder too. In a monolith, the page is “the app is sad.” In a mesh, the page is “checkout is sad” and the cause is a retry storm in a dependency you do not own. You need better tracing — I have used Honeycomb and Grafana Tempo — and you need the discipline to not page every service on every symptom. That discipline is a cost. Budget it or stay in one process.
What I would undo first if I inherited the over-split
I would not rewrite. I would pick the chatty pair — usually “API gateway” plus “core” plus “users” that share a database and a release anyway — and merge them into one deployable. Keep the package names. Drop the network. Keep the tests. Tell the org the merge is a feature, not a demotion.
I would move shared data access behind one writer. Dual writes are how you get a year of “eventual” that is actually “wrong.” If I need a new service later, I will extract with a replicating pattern I can turn off, not with hope.
I would kill the saga that is really a form. User signup that spans four services is a workflow that belongs in one application transaction plus a job for the email. The saga was a way to feel distributed. The user felt a broken account.
I would leave the worker that is actually a worker. The Go renderer, the image pipeline, the thing that must scale separately and can fail separately — that stays. The test is still: different failure, different owner, measured scale, or a different runtime.
How I decide on a blank page in 2026
Start with one deployable. Put seams in the code where you already argue. Instrument the parts you are afraid of. When a seam has a different owner, a different SLO, or a different runtime — and you can name the incident you are preventing — extract that seam. Extract it as a small service with a boring contract. Do not extract the rest to keep the architecture “consistent.” Consistency is how a good extraction becomes an ideology.
Use the platform you already have. Kubernetes is fine if you already run it well. A couple of Fly.io machines or a Kamal-deployed box is fine if you do not. The orchestrator is not the architecture. I have seen beautiful Helm charts around a bad split and a systemd unit around a good one. I know which I would rather page.
Microservices are a tool for isolation. Isolation is expensive. I will pay for it when the alternative is a worse incident or a blocked team I can point to. I will not pay for it because a conference talk used the word “bounded context” as if it meant “new repo.” Bounded contexts can live in a folder. Repos are for when the folder started to lie.
So: do they make development harder? Yes. Locally, in CI, in debugging, in product changes that cross a noun. Sometimes that hardness is cheaper than the hardness of a monolith that cannot deploy, cannot scale a worker, or cannot let two teams ship on a Tuesday. I only write the check when I can say which of those I am buying. If I cannot say it, I keep the monolith and I make the modules honest instead.