Solving the Problem of Monoliths: Microservices (Part 2 of 2)
This 2-Part article introduces Microservices and Domain-Driven Design (DDD) as a method aligning software with business needs using a common language and clear boundaries. It contrasts microservices architecture with traditional monolithic structures, highlighting its scalability, flexibility, and resilience. Additionally, it discusses the integration of serverless computing with containers, emphasizing efficiency, scalability, and cost-effectiveness. This combination of DDD, microservices, and serverless with containers offers a robust framework for developing business-aligned and technically sound software.
Part 1 enumerated why Monoliths are such a pain, so I won't go into much detail here.
Monolithic architectures work beautifully, offering simplicity and straightforwardness, until they become too bulky, too rigid, and too complex to maneuver. It's the classic tale of 'too big to fail' until, well, they do.
Complexity Catastrophe: Monolithic code can resemble a tangled web of confusion, making maintenance feel like a maze.
Tech Trap: You're stuck with the tech choices you made ages ago, like being sentenced to outdated fashion trends.
Scalability Snare: Scaling a monolith often means inflating the whole thing, even when just a part needs a boost.
Testing Tangles: Testing in a monolith can resemble a game of Jenga, where one change might topple the tower.
Tech Debt Trouble: Over time, monoliths accumulate tech debt like dust on an old bookshelf.
Single Sore Spot: Monoliths are a house of cards; when one part fails, the entire structure can come crashing down.
Resource Rampage: They're resource hogs, using more than they need, like a car that always guzzles gas.
Development Drag: Large monoliths can slow down development, making agility a distant dream.
Team Teetering: Growing your dev team with a monolith is like trying to fit everyone in a phone booth – it doesn't work.
Breaking Free
I'm confident that I am not the only person to have wrestled through sleepless nights, haunted by a monolith whose codebase is so tightly coupled that compiling had, at best, a 50/50 chance of success.
Breaking free from the constraints of a monolithic application can be a liberating journey for your organization. I have often referred to this transition as a path to freedom from tyranny. In this grossly over-simplified guide, I'll do my best to avoid going into so much detail that my audience resorts to throwing rotten TLDR;s at me -- but sufficient detail so others can do likewise.
First, the Hard Truth: Monoliths. They work, until... they don't.
Then, the Epiphany: That lightbulb moment. Microservices: Where every service, a standalone unit, nimble and efficient, stable.
Success? Not Overnight: It turns out pulling the plug is not as easy as it sounds.
The Payoff? No more PerfMon sessions of 5GB of inexplicable non heap memory consumption and thread counts spiking from the teens in to the hundreds, and oh those cascading hard faults that only triggered once everyone had gone home for the weekend. Now, my faith in predictability is restored and I no longer have some vague sense I'm trapped in a cosmic game of whack-a-mole.
The path to success is not a straight line. You find yourself painstakingly going line by line through legacy code - not as a grand, omnipotent Agent Smith but more like Tom Hanks in 'Cast Away,' after the loss of Wilson.
It is my hope that these steps will empower you to take control of your software landscape and unlock new possibilities for innovation and growth.
The Game Plan
1. Know Thy Business Domains: Get acquainted with the different parts of your system. Think of it as drawing the map of your new kingdom.
2. MVP: Minimum Viable Freedom: Prioritize what you need to overthrow the old regime. Your MVP is like the rebel leader – basic but effective.
3. Microservices with a Purpose: Build microservices like specialized units in your agents of change. Each has its own mission, and they work together like a well-coordinated team.
4. The API Façade: Your Command Center: Develop an intermediary layer to give commands - like a general orchestrating troops. Redirect the traffic and take control.
5. Incremental Insurrection: Start small, chip away at the monolith's territory one piece at a time.
6. Reuse, but Revolt: Salvage what you can from the old codebase, but be cautious. You don't want to bring along the old liabilities.
7. Data Exodus: Migrate your data systematically - it's like the treasure you're smuggling out. Keep it safe!
8. Quality Assurance Shakedown: Test your microservices rigorously.
9. Monitor and Refine Your Freedom: Keep an eye on your liberated zones. Collect intelligence and adapt to changing conditions.
10. Iterate and Decommission Dictatorship: Continue the culture shift, adding more microservices and dismantling the monolith piece by piece.
Here are my proposed steps to a new era of software freedom.
Decomposing the System
Isolate and migrate that business logic , Follow Clean Architecture, centering on a domain-centric model.
"The first rule of moving to microservices: (this would be right up there with feeding Gizmo after dark), if you can't get modularity right in a monolith, you won't with microservices. Master modularity first, or you'll face the same challenges in a different form."
In the world of DDD-oriented microservices, think of each bounded context as a microservice treasure chest filled with entities, value objects, and aggregates. Your goal: Keep those microservice boundaries compact, just like treasure chests, striking a balance between 'micro' and 'service.' The trick is to decompose until you notice microservices gossiping more than a group of chatty parrots at a party. Cohesion is your compass within each bounded context.
At first, your app is a grand mix of Presentation, Persistence, and Infrastructure layers—a real three-ring circus. But during migration, relocate the data model and domain logic to the Domain layer. Think of the Application layer as the conductor, orchestrating the show with Inversion of Control (IoC) and Dependency Injection (DI) as the magician's tricks.
Identify Shared Services: Create shared services for common functionalities spanning multiple domains.
Refactor for Modularity: Enhance modularity by refactoring legacy code, making domain distinctions clearer.
Bounded Context: Apply DDD's bounded context to define clear boundaries for cross-domain functionality.
API Gateway or Facade: Implement an API Gateway/Facade layer to manage cross-cutting concerns and coordinate calls between domains.
Saga Pattern: For distributed transactions, use a saga pattern to ensure data consistency.
Event-Driven Approach: Adopt an event-driven architecture for functionalities affecting multiple domains.
Gradual Decomposition: Initially, expose functionality via APIs within the monolith, decompose it as domain understanding evolves.
Saga: a solution for handling distributed transactions
Use Case
In a microservices architecture, each service typically manages its own database. This isolation ensures that services are decoupled and independently maintainable. However, this setup can lead to challenges when a business process requires a transaction that spans multiple services—these are known as distributed transactions. To handle these complex scenarios without sacrificing the autonomy of individual services, a robust solution is required to manage transactions that span multiple services.
Solution
The Saga Pattern offers a strategic solution to manage distributed transactions across multiple microservices. According to this pattern, each distributed transaction is broken down into a series of smaller, local transactions. Each local transaction makes its update to the database and then publishes a message or event to trigger the next transaction in the sequence. This approach helps maintain data consistency across services without relying on a centralized transaction coordinator.
If a local transaction fails—for instance, due to a business rule violation—the Saga Pattern ensures consistency by executing compensating transactions. These compensating transactions are designed to revert the changes made by the previously executed local transactions, thus restoring the system to its initial consistent state.
Coordination Methods
Choreography: In this approach, each service performs its local transaction and then publishes domain events that naturally trigger subsequent transactions in other services. This method relies on event-driven communication and does not require a central coordinator.
Orchestration: This method uses a central orchestrator service that explicitly directs each service on the execution of its local transactions. The orchestrator manages the sequence and decision-making process, providing a more controlled but centralized transaction management approach.
Estimating Microservice Domains
Estimating domains for a microservices architecture can be tricky, but Domain-Driven Design (DDD) can help:
Estimation Steps
Understand Business Capabilities: Start by grasping your monolith's core business functions; these often hint at potential domains.
Identify Sub-Domains: Break down larger functions into smaller sub-domains through discussions and workshops.
Bounded Contexts: Define bounded contexts that encapsulate these domains, helping clarify their limits and interactions.
Model Domains: Create visual domain models to visualize relationships between domains and bounded contexts.
Prototype and Iterate: Validate your domain boundaries with prototypes, and be open to adjustments.
Larger vs. Smaller Domains
Larger Domains, Fewer Microservices:
Pros: Simpler management, less communication overhead, easier transactions.
Cons: Risk of becoming mini-monoliths.Smaller Domains, More Microservices
Pros: Flexibility, scalability, focus on specific functions.
Cons: Increased orchestration complexity, network overhead, potential sprawl.
Avoiding Sprawl
Avoid Over-Partitioning: Don't create too many microservices; not everything needs a separate service.
Regular Review: Periodically review architecture, merge or split services as needed.
Business Alignment: Ensure each microservice aligns with specific business goals.
Monitoring: Use metrics to monitor complexity and performance.
Feedback Loop: Keep a feedback loop with teams and domain experts.
Governance: Establish governance and guidelines.
Integration Points: Focus on service integration points; complex integrations may indicate sprawl.
Monoliths are Difficult to Unravel
In case you are new to this...
If this is your first time with microservices, watch out for these pitfalls:
The Distributed Monolith: Beware of tightly coupled services that mimic a monolithic setup by requiring synchronous communication.
Over-Engineering: Don't go overboard by creating too many complex microservices that become challenging to manage and scale.
Ignoring Data Management: Pay attention to how data is managed across services to prevent data integrity issues.
Inadequate Service Boundaries: Properly define service boundaries to avoid services that are either too large (mini-monoliths) or overly granular.
Neglecting API Versioning: Implement and manage API versioning to prevent compatibility issues during service updates.
Improper Cross-Cutting Concerns: Ensure consistent handling of cross-cutting concerns like logging, monitoring, and security across services.
Inadequate Testing Strategies: Adopt comprehensive testing, including unit, integration, and end-to-end testing for each microservice and their interactions.
Insufficient DevOps Practices: Recognize the importance of robust DevOps practices, including CI/CD, for managing multiple independently deployable services.
Failure to Plan for Failure: Design with failure in mind by implementing circuit breakers and fallback mechanisms to prevent cascading failures.
Premature Optimization: Avoid optimizing individual services before understanding their performance characteristics and load patterns.
Lack of Observability: Implement sufficient logging, monitoring, and tracing to diagnose issues effectively in a distributed system.
If you've done this a few times...
Complex Facade Management: Handling the complexity of the intermediary layer that routes traffic between the old and new systems can be daunting.
Data Consistency Balancing Act: Achieving and maintaining data consistency between the monolithic database and microservices' databases is a delicate balancing act.
Performance Hiccups: Initially, system performance might suffer due to the coexistence of the monolith and microservices.
Cultural and Technical Adaptation: The team needs to adapt to new technologies and methodologies, necessitating training and mindset shifts.
Legacy Code Refactoring: Assessing and refactoring legacy code to fit the new architecture can be a time-consuming process.
Principles of Design
In microservices architecture, whether services should be stateful or stateless depends on the specific requirements of the system. What about hosting scenarios? Each decisions has its own advantages and trade-offs.
Stateless Microservices
Advantages: Scalability, Resilience, Simplicity, Compatibility with Serverless.
Use Cases: Independent requests like CRUD operations in RESTful APIs.
Stateful Microservices
Advantages: Performance, Context-Awareness.
Challenges: Complexity, Resilience.
Use Cases: Complex transactions, real-time processing, user sessions.
Multi-Threading, Parallelism vs ASYNC
When deciding between parallelism and asynchronous calls in multi-threaded applications, consider the following:
Parallelism
Use Case: Ideal for CPU-bound tasks where you want to leverage multiple CPU cores for simultaneous computation.
Characteristics: Executes multiple operations concurrently in separate threads, speeding up CPU-bound tasks.
Example Scenario: Data processing, complex calculations, or tasks that can be divided and executed concurrently.
Considerations: Not suitable for I/O-bound operations, introduces synchronization complexity, and may have overhead for smaller tasks.
Asynchronous Calls
Use Case: Best for I/O-bound tasks or when waiting for external resources (e.g., file, network, or database operations).
Characteristics: Non-blocking operations that free the calling thread to perform other tasks while waiting for completion.
Example Scenario: Web service calls, file I/O, or database queries where the application waits for a response.
Considerations: Reduces the need for idle threads, improves scalability, and requires careful handling of callbacks or promises.
Combining Both
In many real-world applications, you may use a combination of both parallelism and asynchronous calls. For example, use asynchronous processes that employ parallel algorithms for data processing upon receiving data.
Decision Factors
Task Nature: Choose parallelism for CPU-bound tasks and asynchronous calls for I/O-bound tasks.
Scalability & Responsiveness: Asynchronous calls often enhance scalability and application responsiveness.
Complexity: Be aware that parallelism can introduce synchronization complexity due to threading.
Environment & Frameworks: Consider your environment and framework capabilities, as some may be better suited to one approach over the other.
Make your choice based on task characteristics and desired outcomes.
Orchestration
Orchestration Engine: Harness a dedicated orchestration engine like Apache Airflow or AWS Step Functions for complex workflows and monitoring.
Centralized Orchestration Service: Create a central service to conduct operations across microservices.
Define Clear Workflows: Document workflow steps from start to finish.
API Gateway: Route requests with an API Gateway, especially for simpler orchestrations.
Asynchronous Communication: Favor asynchronous methods (message queues or event streams) over sync calls for resilient, decoupled services.
Event-Driven Architecture: Let services emit events that trigger orchestrator workflows, enabling dynamic interactions.
Saga Pattern: Implement Sagas for distributed transactions, with choreography or orchestration-based coordination.
Error Handling: Build robust error handling and compensation mechanisms.
Scalability and Performance: Ensure the orchestration layer scales and performs well, avoiding bottlenecks.
Loose Coupling: Keep orchestrators loosely coupled to services.
Versioning and Flexibility: Version workflows and stay flexible to evolving business processes.
Hosting Scenarios
Serverless Architecture: Ideal for stateless microservices, auto-scaling, pay-per-use pricing.
Named Hosts (Servers or Clusters): Suitable for both stateless and stateful services, provides control over the environment
Containers
One Service, One Container: Deploy each microservice in its container for isolation and scalability.
Lightweight Containers: Keep containers lean with minimal dependencies for faster build, deployment, and enhanced security.
Official Base Images: Prefer trusted, regularly updated official base images for your containers. Container Orchestration: Manage containers efficiently with tools like Kubernetes, Docker Swarm, or Amazon ECS.
Data Storage Options
Database per Service: Each microservice has its private database, ensuring independence.
Shared Database: Multiple services share a single database, simpler but risks coupling.
CQRS: Separate models for updates and reads, optimizing performance and security.
Event Sourcing: Store state as events, great for audit trails and consistency.
Saga Pattern: Manage data consistency with distributed transactions using a sequence of local transactions.
Materialized View: Create read-optimized views, handy for CQRS-following microservices.
Recommendations
Prefer stateless design for most microservices due to scalability and simplicity.
Use stateful services when application logic demands it.
Opt for serverless hosting for stateless microservices with variable loads.
Consider container orchestration platforms like Kubernetes for stateful services or greater infrastructure control.
Yes, you can use a Service Fabric with microservices. Service Fabric is a distributed systems platform that makes it easy to package, deploy, and manage scalable and reliable microservices and containers.
Past Learned Lessons
Identify Independent Modules: Find standalone parts of the monolith for easier migration.
Minimize Changes Initially: In the beginning, keep code changes minimal to get the app running in the new environment.
Modularize the Monolith: Gradually break down the monolith into manageable pieces, even before full microservices.
Use Feature Flags: Implement feature flags for gradual testing and migration in a live environment.
Environment Parity: Make the new environment as similar to the old one as possible to reduce compatibility issues.
Configuration Management: Keep config separate from code for easier adaptation to the new environment.
Phased Rollout: Take a phased approach, starting with a subset of users to monitor performance before full rollout.
Containers are Great, Until They're Not
Management Maze: Orchestrating heaps of containers is like herding cats. Wrangle them with proper tools and wizardry.
Networking Knots: Container networking is a puzzle; it's like untangling headphone wires. Trickier in large deployments.
Data Dilemmas: Managing data in stateless containers? It's like juggling flaming torches. Requires careful planning.
Resource Rumble: Containers on one host fighting for resources? It's a resource showdown. Allocate and isolate wisely.
Security Spells: Containers can open security trapdoors. Watch out for unpatched images and hidden runtime gotchas.
Monitoring Mayhem: Gathering logs and metrics from container chaos? It's like finding a needle in a haystack.
Container Drift: Beware of 'container drift,' where containers decide to dance to their own tune, not following their source images.
Things You Should Avoid
Avoid central orchestration & BPM tools (as a general rule)
NO GOD SERVICES
Steer clear of common shared databases
Don't implement a single shared domain model across services
That concludes the article for the majority of our readers. However, for the 2 or 3 engineers who crave a deeper dive into the technical details, the next section was written just for you.
Technical Details
Ideal Technology Stacks
Service Fabrics
Microservice Communications
Effective Retry Patterns
Ideal Technology Stacks
Choosing the right technology stack for microservices depends on project needs, team expertise, and existing infrastructure. Here's a brief overview:
Programming Languages:
Java: Mature, Spring Boot, Micronaut, Quarkus.
Go (Golang): Lightweight, efficient, strong concurrency.
Python: Easy, Flask, FastAPI, Django REST Framework.
Node.js: Non-blocking, Express, NestJS, Koa.
C#: Robust, ASP.NET Core.
Rust: Memory safety, performance.
Infrastructure & Orchestration:
Docker: Standard containerization, consistent environments.
Kubernetes: Scalable orchestration, complex.
Serverless: AWS Lambda, Azure Functions, Google Cloud Functions.
Databases:
Relational: PostgreSQL, MySQL (complex queries, ACID).
NoSQL: MongoDB, Cassandra (scalability, flexible schema).
API Gateway:
NGINX, Zuul, Kong: Manage API requests and routing.
Messaging Systems:
RabbitMQ, Apache Kafka: Asynchronous and event-driven communication.
Monitoring & Logging:
Prometheus, Grafana, ELK Stack: For monitoring and logging.
Consider project requirements, team skills, community support, and long-term sustainability when selecting your stack.
Service Fabrics
Service Fabric is a versatile platform for managing microservices, offering several key features:
Microservices Orchestration: Service Fabric simplifies complex tasks like service discovery, load balancing, and failover.
Stateless and Stateful Services: It accommodates both stateless and stateful microservices, reducing reliance on external data storage.
Scalability and Reliability: Service Fabric ensures high scalability and reliability by managing service placement across a cluster of machines.
Health Monitoring: Robust health monitoring and diagnostics capabilities aid in microservices maintenance and troubleshooting.
Container Support: It seamlessly deploys containerized applications, including Docker containers, for consistent microservices deployment.
Programming Model Flexibility: Service Fabric supports various programming models and languages, offering versatility.
Upgrade and Patching: Advanced features enable microservices upgrades and patches with minimal downtime.
Integration with Development Tools: Service Fabric integrates with development tools and supports CI/CD pipelines.
Considerations when using Service Fabric include:
Complexity: Setting up and managing Service Fabric can be intricate, especially for smaller applications or inexperienced teams in distributed systems.
Learning Curve: A learning curve exists to harness the platform effectively for microservices.
Infrastructure Requirements: Depending on deployment, managing underlying infrastructure may be necessary. It's also available as a managed service in cloud environments like Azure.
Vendor Lock-in: Using Service Fabric may entail vendor lock-in, particularly with heavy reliance on specific features or integrations.
Microservice Communication & Best Practices
Communicating between microservices is essential in microservice architecture, and it's not an anti-pattern. However, the approach you choose matters. Here's how to do it right:
Communication Approaches:
RESTful HTTP APIs: Common for direct, synchronous communication.
Asynchronous Messaging: Use message queues (e.g., RabbitMQ, Kafka) for decoupling and handling variable loads.
gRPC: For high-performance, cross-language communication.
GraphQL: Aggregates data from multiple services into a single response.
Event-Driven Architecture: Emission of events that other services react to.
Best Practices:
API Gateways: Route requests and aggregate responses through a single entry point.
Service Discovery: Implement mechanisms to dynamically discover service locations.
Circuit Breakers: Prevent one failing service from affecting others.
Versioning: Manage API changes for backward compatibility.
Security: Secure inter-service communication using SSL/TLS, OAuth, or tokens.
Loose Coupling: Ensure services remain independent and loosely coupled
Idempotent Operations: Design operations to be idempotent for simplicity and stability.
Retry Patterns
In a microservices architecture, implementing reliable retry patterns is essential for system resilience. Here are some key practices:
Exponential Backoff: Increase retry delay exponentially to prevent overwhelming the system.
Randomized Exponential Backoff: Add random delay to avoid simultaneous retries (thundering herd problem).
Circuit Breaker Pattern: Stop retries after repeated failures; periodically test to check for issue resolution.
Timeouts: Set appropriate timeouts for retry attempts to prevent indefinite hangs.
Maximum Retry Limit: Define a maximum retry count to avoid infinite loops; consider moving failed requests to a dead letter queue.
Idempotency: Ensure operations can be retried safely without unintended effects.
Jitter: Introduce randomness in retry intervals to reduce synchronized retries.
Fallback Strategies: Implement fallback mechanisms for unsuccessful retries, like returning defaults or fetching cached data.
Selective Retries: Be selective about which errors are retried, avoiding non-retriable issues.
Monitoring and Alerting: Monitor retry attempts and failures; set up alerts for unusual patterns.
Context-Aware Retries: Adjust retry strategies based on context, such as time of day or system load.
Testing and Simulation: Test retry logic under various failure scenarios, including network issues.
Documentation and Communication: Document retry policies and communicate them to the team.
Distributed Tracing: Implement distributed tracing to track retries across services for debugging and analysis.
Copyright © 2024 1to1agilecoaching.com.
At 1to1agilecoaching.com, we offer specialized and tailored 1:1 Agile Coaching, perfectly suited to your unique needs. Our services encompass a comprehensive range of Agile Coaching options, from Professional Agile Coach guidance to Certified Agile Coaching and in-depth Agile Coach Training. Emphasizing the convenience and effectiveness of digital interaction, we provide top-tier Virtual Agile Coaching Sessions and Personalized Agile Coaching Online. Our expertise is broad and deep, covering areas like Scrum, custom Kanban methodologies for Agile Teams, and the intricacies of Scaled Agile Frameworks (SAFE). As seasoned Agile Practitioners, we excel in Agile Project Management and nurture skills through our Agile Mentoring Programs. We also offer robust training programs, including Licensed Agile Coach Training and certification for aspiring Agile Coaches. Understanding the diverse needs of our clients, we offer Agile Coaching Flex Sessions for added flexibility. Committed to affordability, we ensure our Agile Coaching solutions are both cost-effective and high-impact. At the core of 1to1agilecoaching.com is our dedication to providing Agile Coaching that is not just generic but Customized to your specific individual and organizational objectives, ensuring maximum relevance and efficacy.