Cloud Native 25 min read

From Monolith to Resilient Microservices: A Step‑by‑Step Architecture Evolution

The article walks through a real‑world online supermarket project, showing how a simple monolithic system evolves into a fully‑featured microservice architecture, detailing each refactoring stage, the problems encountered, and the concrete solutions such as service extraction, database sharding, monitoring, tracing, gateways, service discovery, reliability patterns, testing, and service‑mesh adoption.

Architect
Architect
Architect
From Monolith to Resilient Microservices: A Step‑by‑Step Architecture Evolution

Initial Monolithic Design

To understand microservices, the author first explains what a monolithic application is: a single deployable unit that bundles all functionality. Using a fictional online supermarket built by "Xiao Ming" and "Xiao Pi", the initial requirements are a public website for browsing and purchasing products and an admin backend for managing users, products, and orders.

The first architecture consists of two separate applications (website and admin backend) deployed on a single cloud VM, with a shared database. This simple setup works quickly because the team can develop features without much planning.

Rapid Expansion and Pain Points

When competition intensifies, the team adds promotions, a mobile app, and data‑analysis features. Development speed leads to ad‑hoc decisions: promotional and analytics logic are placed in the admin backend, the mobile app is built separately, and code duplication appears across the website and mobile client.

The resulting architecture suffers from duplicated business logic, tangled API calls, oversized services, performance bottlenecks in the admin backend, a single shared database that becomes a hotspot, and deployment complexity that forces nightly releases at off‑hours.

Service Extraction and Shared Database Issues

Recognizing the problems, the team abstracts common capabilities into five public services: User, Product, Promotion, Order, and Data‑Analysis. Each backend now calls these services instead of containing duplicated code, leaving only thin controllers and UI layers.

Although services are separated, the database remains shared, so issues such as a single point of failure, schema coupling, and cross‑service data access persist.

Database Sharding and Messaging

To eliminate the shared‑database bottleneck, the team shards the database: each service owns its own persistence layer. They also introduce a message‑queue (e.g., RabbitMQ or Kafka) to decouple asynchronous communication.

Now services can use heterogeneous storage—Data‑Analysis may use a data‑warehouse, while Product and Promotion add caching for high‑frequency reads.

Monitoring and Observability

High‑concurrency microservices require a robust monitoring stack. The author adopts Prometheus as a metrics collector and Grafana for dashboards, using exporters such as RedisExporter and MySQLExporter to expose component‑specific metrics.

Tracing and Logging

To locate failures across service boundaries, the team implements distributed tracing. They follow the Istio example, injecting traceId, spanId, parentId, and timestamps into HTTP headers, and send logs to Zipkin (an open‑source Dapper implementation).

Tracing reveals the request flow: a user accesses productpage, which calls details and reviews, and reviews further calls ratings. The trace forms a tree that can be visualized.

For log analysis, the team uses the ELK stack (Elasticsearch, Logstash, Kibana). Logs are collected by agents on each host and shipped to Logstash, avoiding code changes in the services.

Gateway and Access Control

A gateway is introduced to centralize routing, authentication, and API documentation. The coarse‑grained approach places a single gateway in front of all services, simplifying external access while allowing internal calls to bypass the gateway for performance.

Service Discovery and Autoscaling

Dynamic registration and discovery (e.g., Consul, Eureka, Zookeeper, etcd) replace static host lists. Services register themselves on startup, and a health‑check loop removes unhealthy instances. Autoscaling becomes a two‑step process: deploy a new instance and register it; removal is automatic when the instance stops responding.

Reliability Patterns

The team adds redundancy (multiple instances per service) and implements circuit breaking, service degradation, and rate limiting. Circuit breaking marks a failing downstream service as unavailable after repeated errors, preventing request pile‑up. Degradation disables non‑critical features (e.g., recommendation) when the dependent service fails. Rate limiting protects upstream services from being overwhelmed by a burst of requests from a misbehaving downstream service.

Testing Strategies

Testing is divided into three layers: end‑to‑end (full system), service‑level (API contracts), and unit tests (code units). End‑to‑end tests are applied to core flows; failures are traced back to unit tests for rapid regression detection. Service tests use mock servers to isolate dependencies, while unit tests aim for high coverage.

Unified Microservice Framework

To avoid repetitive integration code (metrics, tracing, logging, registration), the author builds a lightweight framework that encapsulates these concerns. All services import the framework, reducing boilerplate and ensuring consistent behavior.

The downside is that framework upgrades require coordinated updates across all services, demanding strict version‑management policies.

Service Mesh and Sidecar

As an alternative to a custom framework, the author explores a Service Mesh architecture. Each service runs alongside a sidecar proxy that handles all inbound/outbound traffic, providing transparent metrics, tracing, and security without code changes. The mesh consists of a data plane (sidecars) and a control plane (configuration distribution).

While sidecars simplify upgrades and reduce intrusion, they introduce memory‑copy overhead and potential performance penalties.

Conclusion

The journey demonstrates that microservice adoption is iterative: start with a monolith, extract services, address shared‑resource pitfalls, and progressively add observability, resilience, and automation. The author emphasizes that architecture evolution never truly ends—future directions include serverless, FaaS, or even a return to well‑engineered monoliths.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

monitoringCloud NativearchitecturetestingService Meshtracing
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.