From Monolith to Microservices and Containerization: A Senior Architect’s Journey
The article chronicles a SaaS team's evolution from a two‑person monolithic application to a microservice‑based, containerized architecture, detailing API design, CI/CD practices, Spring Cloud and Kubernetes integration, automated deployment, tracing, and monitoring strategies.
In this first‑person account, a senior architect explains why microservices may or may not suit small teams, emphasizing that growing business complexity makes splitting a large monolith into smaller services a natural solution.
The early stage involved a two‑person team building a SPA with front‑end/back‑end separation, using Nginx to proxy static assets and API requests to a server on port 8080.
API design follows RESTful conventions, using versioned paths such as /api/v2 , resource‑oriented URLs like /api/contacts or nested forms like /api/groups/1/contacts/100 , and standard HTTP verbs (POST, PUT, DELETE, GET). The team generates Swagger documentation for front‑end consumption.
Continuous integration (CI) was introduced early, with integration tests that exercise APIs, databases, and message queues, executed automatically by Jenkins. This provided confidence for refactoring while maintaining API compatibility.
During the microservice era, services were split based on low‑coupling database tables or domain‑driven design (DDD) boundaries, aiming for clear service boundaries without circular dependencies.
The team chose Spring Cloud (Zuul, Eureka, Hystrix, Feign, Ribbon) as the microservice framework, noting its limitations for heterogeneous languages and the performance impact of Zuul’s synchronous model.
Architecture transformation continued with the addition of a Spark‑based big‑data analytics layer, expanding data sources from MySQL to Elasticsearch and Hive.
Containerization was planned from the start; each service now has a Dockerfile, CI builds Docker images and pushes them to Harbor, and Kubernetes (OpenShift) replaces Eureka for service discovery, using native services and load balancing.
Spring Cloud and Kubernetes integration required disabling Eureka client ( eureka.client.enabled=false ) and configuring Ribbon to use static server lists, e.g., foo.ribbon.listofservers=http://foo:8080 .
CI was extended to build Docker images and handle database migrations. The previous Flyway‑based approach was replaced with a containerized, run‑once migration tool invoked via docker run --rm , supporting version‑targeted upgrades.
Automated deployment leverages Jenkins pipelines and Ansible to transfer JARs to jump hosts before deploying to the cluster, respecting service dependency order.
For tracing, the team uses RequestId and TraceId headers, propagating them via ThreadLocal across service calls and message queues, enabling rapid troubleshooting of failures.
Operational monitoring relies on Telegraf, InfluxDB, and Grafana to collect JVM, system, and MySQL metrics, with Spring Boot Actuator exposing endpoints for JMX.
The article concludes that each of these topics could be expanded into full articles, highlighting the collaborative effort required across development, testing, and operations to evolve a system from monolith to microservices and containerized deployment.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.