The Journey to the Microservices

Alexander Ibrahim

--

Situation: The Monolithic System and Deployment Challenges

When I joined the company was operating on a monolithic architecture. This meant that all of our services — APIs, website frontend, and background jobs — were housed in a single repository. While this setup worked in the company’s early stages, it started to show significant limitations as the business grew.

Deployment Process Using Elastic Beanstalk

At the time, we were deploying our system using Elastic Beanstalk, which automated infrastructure tasks like scaling and monitoring. However, because all services were bundled together in a monolithic system, every update required a full redeployment of the entire application, even for minor changes. This created unnecessary complexity and increased the risk of errors.

Deployment Bottlenecks: Limited Access, Big Delays

Additionally, only a small group of senior engineers had the authority and access to deploy code to production. This created several bottlenecks:

  • Limited Deployment Frequency: With only a few people responsible for pushing updates, we couldn’t deploy as often as we wanted. Even though developers could finish their tasks, the delay in deployment held back progress and slowed the release cycle.
  • High Risk of Errors: Because every change required a full redeployment of the entire system, there was a higher chance of errors during deployment. Even small changes could introduce issues that affected unrelated parts of the system, making rollbacks more complicated.
  • Dependency Management Issues: All the services (API, frontend, background jobs) shared the same dependencies. This meant that upgrading a dependency for one service could potentially break others, leading to additional testing overhead before every deployment.

The Growing Pains of a Monolithic System

As the company expanded and added more features, these issues began to significantly impact our productivity:

  • Slow Development: Developers had to wait for one of the few authorized engineers to deploy code, slowing down the entire development cycle.
  • Lack of Flexibility: With all services bundled together, scaling individual components (like the API) was impossible. If the API needed to handle more traffic, we had to scale the entire application, which was inefficient and costly.
  • Increased Downtime Risks: Every deployment carried the risk of bringing down the entire system. If something went wrong, diagnosing and fixing issues in the monolithic codebase took time, increasing the likelihood of downtime.

It became clear that our monolithic system and deployment process were not sustainable for long-term growth. As we looked to improve scalability, reduce deployment bottlenecks, and enable more frequent, safer releases, we knew it was time to consider moving to microservices.

Task: Moving to a Microservices Architecture

The company recognized the need to shift to a more modern architecture that allowed for greater scalability and flexibility. The task assigned to me was to lead the migration from a monolithic system to microservices. This involved:

  • Breaking down the tightly coupled services into independent, standalone services.
  • Ensuring minimal disruption to our existing operations.
  • Demonstrating the effectiveness of microservices through proof of concept, documenting the process, and delegating the implementation to other teams.

3. Action: How We Implemented Microservices

The transition to microservices required careful planning and execution. Here’s how we approached the migration:

Service Identification

We started by identifying services that could be safely decoupled from the monolithic system. Our first focus was on the product attributes service, as it posed the least risk, had a well-defined domain, and involved a smaller scope compared to other parts of the system. Since product attributes were largely independent, separating this service allowed us to test the effectiveness of the microservices approach without significantly disrupting the rest of the system.

This initial step laid the groundwork for decoupling other, more complex services in the future.

Decoupling Services

Once we identified product attributes as the starting point, we began the process of decoupling it from the monolithic system. This involved carefully rewriting portions of the codebase to separate concerns and ensure that this service could operate independently. We focused on the product attributes domain first to minimize risk while proving the concept.

Instead of creating separate repositories for every service right away, we initially worked within a single microservice domain for product attributes. This allowed us to refine our approach before expanding to other services. By working in one domain, we ensured a smoother transition without overwhelming the team or disrupting the overall system.

Deploying to Elastic Beanstalk

After decoupling the product attributes service, the next step was to deploy it to Elastic Beanstalk. We used this deployment as a proof of concept to understand how the microservice would behave in a real environment. The goal was to test the deployment process, monitor how the service functioned independently, and identify any issues related to infrastructure, scalability, or communication with the rest of the system.

This deployment allowed us to observe firsthand the benefits and challenges of the microservices approach and helped us refine our strategy for future services.

Containerization and Orchestration with Docker Swarm

Once the service was running, we moved on to containerization and orchestration. For this, we chose Docker Swarm due to its gentle learning curve compared to Kubernetes. Although our team had never used Docker in a production environment, we found that learning Docker alongside Docker Swarm was much easier than learning Docker while simultaneously tackling the complexity of Kubernetes. The similarities between Docker Compose and Docker Swarm allowed us to transition smoothly, enabling us to:

  • Containerize the product attributes service for easier deployment and scaling.
  • Orchestrate the service using Docker Swarm, handling tasks like load balancing and service discovery without overwhelming the team with Kubernetes’ intricacies.

This approach not only simplified the learning process for the team but also accelerated adoption, allowing us to focus on scaling and optimizing our microservice architecture.

Database Considerations

At this stage, we decided to continue using the existing monolithic database. Our focus was on decoupling the application layer first and ensuring that the microservices were working smoothly before tackling database migration.

This allowed us to reduce complexity and risk while transitioning to microservices. Once the microservices architecture was proven successful and stable, we planned to gradually migrate the database for true service independence, ensuring each microservice could manage its own data without relying on the shared database.

Testing and CI/CD Pipelines

We overhauled our testing and deployment processes by building a self-hosted CI/CD pipeline using Jenkins. This setup allowed us to automate the deployment workflow for each microservice, giving us full control over the integration and delivery process without relying on external cloud providers. Each microservice had its own automated testing suite integrated with Jenkins, which enabled us to quickly run tests and ensure code quality before deploying.

With Jenkins managing our CI/CD pipelines, each service could be deployed frequently and independently, significantly reducing risks to the overall system. This setup improved both our deployment speed and confidence, as it allowed us to release updates more frequently without impacting the stability of other services.

Observability and Monitoring

  • Centralized Logging: We set up centralized logging using tools like ELK Stack (Elasticsearch, Logstash, and Kibana). This allowed us to collect, analyze, and visualize logs from all microservices in one place, making it easier to identify trends, track errors, and troubleshoot issues.
  • Distributed Tracing: To understand how requests traveled through our microservices, we implemented distributed tracing with tools such as Jaeger. This helped us visualize the flow of requests across services, making it easier to detect performance bottlenecks or failures in specific services.
  • Metrics Collection and Monitoring: We utilized tools like Prometheus and Grafana for metrics collection and real-time monitoring. These tools allowed us to track indicators for each microservice, including response times, resource utilization, and error rates. By setting up alerts for abnormal metrics, we could quickly respond to any issues.
  • Health Checks and Alerting: Each microservice included its own health checks to ensure consistent operation. To monitor system health, we used Kuma for tracking uptime and checking the status of individual services. Additionally, we configured Grafana for real-time monitoring and alerting on critical metrics. Grafana’s alerting capabilities allowed us to set up custom thresholds and receive instant notifications for any service downtimes or unusual activity. Together, Kuma and Grafana enabled a proactive approach to monitoring, allowing us to respond swiftly to potential issues and maintain system reliability.

4. Result: The Benefits of Microservices

The results of moving to microservices were immediate and impactful. Here are some of the key benefits we observed:

  • Increased Scalability: Each service could now be scaled independently. If we needed more capacity for our product attributes but not the website, we could scale up the product attributes without touching the website or other services.
  • Faster Deployments: Deployments became faster and less risky. Instead of deploying the entire system, we could release updates to individual microservices without affecting other parts of the system.
  • Improved Fault Isolation: Bugs and failures were easier to isolate and fix. If a bug occurred in one service, it didn’t bring down the entire system, making troubleshooting quicker and more efficient.
  • Technology Flexibility: Moving to microservices gave us the freedom to use different technologies for different services, allowing each team to choose the best tools for their specific needs. For example, we could use Golang for the API while keeping the frontend in React. Additionally, this architecture enabled us to experiment with new technologies like Envoy for service mesh, Kubernetes for container orchestration, KEDA for event-driven scaling, and MongoDB as an alternative database solution for specific services. This flexibility not only optimized each service but also allowed us to explore and adopt tools that best suited our evolving requirements.
  • Better Developer Autonomy: With services decoupled, different teams could work on their microservices independently. This allowed for parallel development, reducing bottlenecks and improving overall productivity.
  • Access to a Broader Talent Pool: Adopting a microservices architecture made it easier for us to attract and hire specialized talent. Since different services could be built using diverse technologies, we could bring on developers with expertise in specific areas like backend, frontend, or containerization. This flexibility made our positions more attractive to skilled professionals and broadened the pool of talent available to our teams.

Why Microservices Matter in Modern Development

Moving from a monolithic architecture to microservices is not just about scalability; it’s about building systems that are more flexible, maintainable, and resilient. In today’s world, where companies need to innovate quickly, the microservices approach offers:

Agility: Teams can move faster and make changes without worrying about breaking the entire system.

Reliability: If one service fails, it doesn’t bring down the rest of the system.

Innovation: Microservices allow teams to experiment with new technologies without impacting the whole platform.

For the company, this transition was a game-changer, making our systems more agile and future-proof.

Conclusion: Embracing Microservices for Long-Term Success

Transitioning from a monolithic architecture to microservices was a transformative journey that addressed critical challenges in scalability, flexibility, and development speed. By breaking down our system into independent, manageable services, we were able to streamline deployments, isolate faults more effectively, and scale services independently, ultimately enhancing system resilience and efficiency.

Not only did this shift reduce deployment bottlenecks and minimize downtime risks, but it also opened up new opportunities for team autonomy, technology flexibility, and attracting specialized talent. Each team could work on specific services using the best tools and technologies for the task, resulting in a more productive and collaborative environment.

This journey demonstrated that adopting a microservices architecture is about more than just technical improvement; it’s a strategic move towards building a system that can adapt and grow with the business. In today’s fast-paced development landscape, microservices provide the agility, reliability, and innovation that are essential for long-term success. For us, it has been a game-changer, future-proofing our systems and positioning the company for sustained growth and adaptability.

Have you considered moving to a microservices architecture? I’d love to hear your thoughts and experiences. Drop a comment below, share your challenges, or ask any questions about our transition. Let’s start a conversation and learn from each other’s journeys!

--

--

Responses (1)