The monolithic revival: why your microservices project is bleeding money comes down to a blunt truth: you probably do not have a distributed-systems problem. You have a product-delivery problem wearing a Kubernetes-shaped hat. Splitting an application into services adds network calls, deployment pipelines, observability, platform ownership and coordination work before it adds a single useful feature.
The architecture industry sold us independence. In practice, most internal services share one database, one release train and the same handful of engineers. That is not microservices; that is a distributed monolith with better branding and a larger AWS bill. Monzo, the BBC and NHS Digital all operate complex estates, but complexity is not a reason to fragment every codebase. It is a reason to earn each boundary deliberately.
A service boundary moves latency and failure modes. A network hop that takes five milliseconds locally can take 50 milliseconds across an availability zone, while a timeout becomes a queue, a retry storm and a page for someone who did not write the failing code. You also pay for the glue: service discovery, load balancers, API gateways, message brokers, distributed tracing, service-to-service authentication, contract testing and several dashboards that describe the platform rather than the customer journey.
The expensive part is rarely the first extraction. It is everything that follows. A team must coordinate database migrations across services, keep API versions compatible, manage environments for every dependency and debug a trace that crosses six systems. Google Cloud's DORA research is useful here: deployment frequency alone is a weak proxy for performance. High-performing teams focus on throughput and stability, not the number of services they can operate.
Amazon's 2014 Builders' Library described the failure modes that emerge when services are coupled through unreliable networks. That remains the uncomfortable truth behind the industry’s distributed-systems boom. If one checkout request depends on inventory, pricing, fraud and notifications, the availability of the whole flow is constrained by the weakest dependency and the most generous timeout.
Compute is visible; engineering time is not. Ten small services may look modest on an AWS monthly invoice, but they demand more CI minutes, ephemeral environments, container images, load-balancer targets, NAT gateway traffic, log ingestion and retained telemetry. They also create on-call toil. An incident that once meant restarting one application can now mean checking five health endpoints, three queues and a service mesh before anyone identifies the bad release.
Use your actual telemetry rather than guessing. This small Python script can group the main cost categories from exported AWS Cost Explorer data:
import csv
from collections import defaultdict
costs = defaultdict(float)
with open("aws-costs.csv", newline="", encoding="utf-8") as file:
for row in csv.DictReader(file):
category = row["category"]
costs[category] += float(row["cost"])
for category, cost in sorted(costs.items(), key=lambda item: item[1], reverse=True):
print(f"{category:28} GBP {cost:,.2f}")
Then add the less visible costs: time spent diagnosing cross-service failures, engineers maintaining deployment tooling, and the delay caused by waiting for another team to publish a compatible change. If the business case for splitting a service does not include those numbers, it is a diagram, not a business case.
Open a typical service diagram and look for shared tables. If the orders service writes to the orders schema while the billing service reads it directly, you have not decoupled the data. You have distributed write contention. If every service deploys once a week because one customer journey spans all of them, you have not gained independent delivery. You have multiplied the release train.
Independent deployability is the test. Extract a service only when it has a clear owner, an explicit interface, autonomous deployment and an operational model somebody genuinely wants. A REST endpoint over shared tables is a module boundary, not a service boundary. That distinction matters because modules are cheap to change. Network contracts are not.
A modular monolith gives you most of the organisational benefits with far less operational friction. Use clear internal interfaces, feature folders and strict dependency rules. Keep domain modules separate in code while allowing them to run in one deployable unit. When a module needs genuine scaling or a different release cadence, extract it then. This is the pragmatic middle ground the microservices debate tends to ignore.
Start with boundaries that duplicate operational work but add no business value. A reporting service reading the primary database through a bespoke API may be perfectly justified, but a notification service deployed separately for a queue that receives ten events per second is probably ceremony. Rank candidates by deployment frequency, resource requirements, failure impact and number of upstream consumers.
Do not attempt a heroic rewrite. Collapse the lowest-risk service into the main application, remove its network endpoint and preserve its public contract temporarily through an in-process interface. Archive the deployment pipeline, consolidate dashboards and remove unused infrastructure. Repeat only when the tests and transaction handling remain trustworthy.
Data needs more care. Do not merge databases simply because two services share a table. First stop unauthorised cross-service writes and introduce an owning module for each schema. Create an outbox pattern where reliable events are genuinely needed, then move consumers behind the same interface. A monolith can still publish events through Amazon SNS, SQS, Google Pub/Sub or RabbitMQ; deployment topology and integration style are separate decisions.
Extract around capabilities with distinct scaling and ownership needs, not around nouns in the org chart. Payment processing may justify isolation because it has compliance requirements and different scaling patterns. A helper that turns a postcode into a region probably does not. Keep shared libraries for genuinely shared technical concerns, but avoid turning every internal package into another versioned dependency.
Make failure visible. OpenTelemetry traces, structured logs and service-level objectives still work in a modular monolith, and you lose fewer details when a request stays inside one process. Use load testing that measures end-to-end latency rather than the speed of each service in isolation. Track deployment frequency, change failure rate, lead time for changes and recovery time, following the DORA model, instead of rewarding architecture diagrams.
Your target architecture should be the least distributed design that satisfies the product, compliance and reliability requirements. That will usually mean fewer deployments, smaller bills and clearer ownership. Microservices can be right for a specific workload, but treating them as the default modern architecture is how teams end up maintaining twenty services for a product that needs one.
Keep a service when it needs independent scaling, has a separate release cadence, requires a different runtime, enforces a meaningful security boundary or is owned by a stable autonomous team. Consumer-facing systems, high-volume data pipelines and payment or identity workloads can meet that bar. Even then, do not split merely because a domain has an interesting name.
The architecture should follow the evidence. If one module causes 90% of compute demand, isolate and scale it while leaving the rest together. If one team owns the customer journey, keep that journey deployable as a unit. If contracts are unstable and changes cross five repositories every week, collapse the repositories first.
Set a threshold for introducing a new deployable service. Require an architecture decision record that names the operational owner, expected scaling difference, failure isolation benefit, estimated monthly platform cost and migration plan. Include the cost of CI, observability, on-call, security patching and incident response. If the answer is “it looks cleaner”, keep it inside the monolith.
Then measure the result after consolidation. Compare cloud spend, deployment lead time, change failure rate, incident duration and engineer hours spent maintaining infrastructure. A successful consolidation may reduce cloud costs by only a modest percentage while releasing an engineer from pipeline and dashboard maintenance. That is still a good return, and it is far easier to defend than claiming that fewer YAML files automatically produce a better product.
Bring back the monolith as a deliberate option, not an ideological retreat. Start with one high-confidence boundary, collapse it carefully and watch whether delivery improves. The monolithic revival is not nostalgia. It is the recognition that distribution is a cost, and architecture should earn it.
How do we know if our microservices are a distributed monolith?
If services share databases, require coordinated releases or fail together because one shared dependency is unavailable, you likely have a distributed monolith. Check whether you can deploy one service without coordinating changes across several others. If you cannot, collapse the weakest boundary first.
Should we always rewrite microservices as a monolith?
No. Rewrite or extract selectively based on scaling, compliance, ownership and technical requirements. A good approach is to move selected modules back into a modular monolith, preserve stable interfaces and keep genuinely independent workloads separated.
What is the first step to reduce microservices costs?
Map services to teams, dependencies, deployment frequency and cloud cost, then identify one low-risk service with duplicated operational overhead. Consolidate it, remove its pipeline and infrastructure, and measure delivery and reliability before doing another.