Most teams reach for microservices after the monolith hurts, and that default is usually wrong for the person carrying the pager. Splitting services can reduce code conflict, but it also multiplies failure modes before it improves reliability. My position: unless production operations are already boring, the safer default is a hardened modular monolith with strict interfaces, measured bottlenecks, and delayed extraction.
Microservices make the pager louder before they make delivery faster
The common promise is that smaller services let teams deploy independently, but that benefit arrives late because the first thing you create is more runtime surface area. One monolith on PostgreSQL 16, NGINX 1.26, and systemd has fewer ways to partially fail than twelve services on Kubernetes 1.29, Helm 3.14, Istio 1.22, Envoy 1.30, and separate data stores, because every hop adds scheduling, discovery, authentication, timeout, retry, and observability work.
A DevOps engineer does not support architecture diagrams; you support DNS, certificates, queues, slow queries, node pressure, and the deployment that reached 80% before the new pods started crashing. Kubernetes sets progressDeadlineSeconds to 600 seconds by default in Deployments, a vendor-published value that is generous enough to hide a bad rollout for ten minutes unless your alerts check rollout health directly. That is not an academic difference during a customer-facing incident.
Distributed tracing with OpenTelemetry Collector 0.102, Jaeger 1.57, or Grafana Tempo 2.4 helps, but it does not remove the operational cost because traces are useful only after every service propagates W3C Trace Context correctly. Prometheus 2.52 and Grafana 10.4 can show RED metrics, but a dashboard full of rate, errors, and duration still leaves you debugging whether the fault is an Envoy retry storm, a Kafka 3.7 consumer lag spike, a PostgreSQL connection pool limit, or a bad OpenAPI 3.1 contract.
The default also shifts failure from obvious to ambiguous. A monolith with a saturated database usually fails in a way the whole team recognizes, while a microservice mesh can fail as HTTP 503 at the edge, gRPC UNAVAILABLE between services, OAuth 2.1 token validation latency, or TLS 1.3 handshake churn. That ambiguity matters because mean time to recovery rises when the responder must identify both the broken component and the ownership boundary.
I would not start by carving the codebase into many independently deployed services, because most teams have not yet proved they can operate one service with clean SLOs, reversible deploys, and reliable telemetry. A measured baseline I want before extraction is an incident MTTR below 30 minutes for the monolith’s top failure modes, because slower recovery usually means the organization lacks the runbooks and observability needed for a larger service graph.
The split-by-domain default hides the worst failure mode: ownership without operability
Domain-driven service boundaries sound mature, but they are a dangerous starting point when production ownership is weaker than code ownership. A team may own “billing” or “accounts” in Jira, but the pager event will involve an ingress timeout, a failed Flyway 10 migration, a Redis 7 eviction policy, and an AMQP 0-9-1 queue depth, because production failures ignore domain vocabulary.
Legacy Application Modernization: Monolith to Microservices is a defensible narrative for funding, but I would not use it as the production plan because a narrative does not decide who rotates certificates, tunes circuit breakers, owns dead-letter queues, or rolls back an incompatible schema change at 02:00.
The popular default assumes service autonomy reduces coordination, but it often moves coordination into runtime because every schema, API, event, and timeout becomes a contract. Pact 4.x consumer-driven contracts can catch some API drift, and OpenAPI 3.1.0 can document request and response shapes, but neither prevents a release from increasing p95 latency when a downstream service adds a synchronous fraud check or a database index is missing.
The numbers should be operational, not aspirational. A 99.9% availability SLO is a tuneable target, not a badge, because it permits about 43.8 minutes of monthly error budget by arithmetic and forces you to ask whether the new service graph can spend that budget without constant escalations. A p95 latency budget of 200 ms at an internal API boundary is a value to tune, because five sequential calls at that budget can consume a full second before the edge service renders anything.
Split-by-domain also tempts teams into database-per-service too early, and that hurts because reporting, reconciliation, and rollback become distributed-systems problems. Debezium 2.6 with PostgreSQL logical decoding can stream changes safely, but it adds offset management, connector monitoring, schema evolution, and Kafka topic retention to your support burden. Two-phase commit through XA is worse for most web systems because it couples availability to the slowest participant, which means a small database problem can block otherwise healthy workflows.
If the existing monolith is slow, prove the cause before changing the topology. Use pg_stat_statements in PostgreSQL 16, JFR on Java 21, async-profiler 3.0, or eBPF tools such as bpftrace 0.20 to locate the constraint, because many “monolith scaling” problems are lock contention, N+1 queries, thread-pool starvation, or cache stampedes rather than architectural destiny.
A modular monolith wins until these production gates pass
The explicit comparison is simple: a modular monolith wins when the main problem is deploy risk, shared database coupling, or unclear ownership, because it lets you enforce module boundaries while keeping rollback, logging, and transactions understandable; its cost is slower independent team release cadence and the need for discipline in code review. Microservices win when different parts need distinct scaling profiles, release cadences, data isolation, or fault containment, because separate runtimes can isolate load and ownership; their cost is a permanent tax in networking, observability, security, and incident coordination.
That comparison is uncomfortable because engineers like the freedom of small services, but freedom without operability is just a larger failure domain. If your team cannot answer who owns an SLO burn alert, who approves a breaking OpenAPI field removal, and who handles a Kafka consumer group stuck after a rebalance, then service boundaries are not autonomy; they are unresolved tickets with endpoints.
Before extraction, I would set gates that a DevOps engineer can verify. Deployment failure rate should be below 5% across the last 20 production deploys, an observed threshold rather than a universal law, because frequent rollback failures become harder when twenty services have version skew. Alert noise should be low enough that one actionable page per service per week feels exceptional, because noisy alerts train responders to ignore the next real SLO burn. The monolith should expose Prometheus metrics, structured JSON logs, and trace IDs consistently, because extracting uninstrumented code only spreads blindness.
The following shell script runs against a Prometheus server and asks for p95 latency from a standard histogram, which is the sort of check I would automate before allowing a service extraction request to pass review:
#!/usr/bin/env bash
set -euo pipefail
PROM="${PROM:-http://localhost:9090}"
SVC="${1:-checkout}"
QUERY='histogram_quantile(0.95, sum(rate(http_server_duration_seconds_bucket{service="'$SVC'"}[5m])) by (le))'
curl -fsG "$PROM/api/v1/query" \
--data-urlencode "query=$QUERY" |
python3 -m json.tool
This small check is not a release process, but it demonstrates the standard I would apply: if the team cannot query latency by service name, it is not ready to multiply services. Micrometer 1.13, OpenTelemetry SDK 1.39, and Prometheus client libraries make this practical, but the hard part is agreeing that missing telemetry blocks architecture work.
A modular monolith is not a synonym for “do nothing,” because it can use package-level enforcement, separate Gradle modules, Maven Enforcer, ArchUnit 1.3, database migration discipline with Flyway, and internal APIs that mimic future service boundaries. It wins for most teams because it reduces accidental coupling without creating network partitions, mTLS certificate rotation, sidecar upgrades, or cross-service rollback planning.
If you still split, split by blast radius rather than org chart
Monolith Scaling Pains Modernize to Microservices should trigger a capacity review before a rewrite, because scaling pain often comes from one hot path that can be isolated without turning the entire codebase into a distributed system.
The first extraction should be boring, reversible, and operationally visible. A good candidate is a component with high load variance, simple data ownership, and an obvious fallback, because the support team can then learn the new failure modes without risking the core transaction path. A bad candidate is a central entity service used by every request, because it creates a synchronous dependency that can take down the whole product when it stalls.
Prefer asynchronous seams where correctness allows them. The transactional outbox pattern with Debezium 2.6 and Kafka 3.7 costs extra storage, connector monitoring, and duplicate handling, but it avoids the worse cost of losing events between a database commit and a broker publish. Use idempotency keys and store processed message IDs, because retries are guaranteed in real systems even when the happy-path diagram omits them.
Be conservative with service mesh adoption. Istio 1.22 gives mTLS, traffic shifting, retries, and telemetry, but it costs CPU, memory, upgrade planning, and another control plane to debug. Linkerd 2.15 is lighter for many clusters because it has fewer knobs, while Istio wins when you need richer traffic policy and integration depth; both cost more than plain Kubernetes Services because every proxy becomes part of the request path.
Timeouts must be designed rather than inherited. AWS Application Load Balancer has a published default idle timeout of 60 seconds, and that value can mask broken downstream behavior because users should rarely wait a minute for a web request. Set client timeouts shorter than server timeouts, cap retries, and add jittered backoff, because synchronized retries can amplify a small outage into a fleet-wide collapse.
Data extraction should start with read models, not ownership transfers, because read-side duplication is easier to roll back than splitting writes. CDC into Elasticsearch 8.14 or OpenSearch 2.15 can serve search-heavy workloads, but the cost is eventual consistency and index repair procedures. If the team cannot tolerate stale reads for a specific path, that path is probably a poor first extraction.
Security also changes shape after the split. OAuth 2.1 and OIDC can standardize identity at the edge, but internal authorization still needs policy decisions, audit logs, token propagation rules, and secret rotation. HashiCorp Vault 1.16 or Kubernetes Secrets with envelope encryption can handle secrets, but either choice fails operationally if rotation is not rehearsed and monitored.
Start tomorrow by choosing one painful path and proving whether topology is the cause. Capture p95 latency, error rate, saturation, deploy frequency, rollback time, and the exact dependency chain for that path. If the bottleneck is local, fix the monolith. If the bottleneck is isolated and observable, extract one service with an owned SLO and a rollback plan.



