Part 2 : Application Architecture
From Monolithic to SOA to Microservices: A Modern Approach
Monolithic architecture was easy to design and implement but it had major issues of Organizational and System Scalability.
As software systems grow, the need for modularity and scalability becomes essential. The journey from Monolithic architectures to Service-Oriented Architecture (SOA) and ultimately to Microservices is a response to the challenges of scalability, deployment, and maintenance in modern software development.
Differences Between SOA and Microservices
While both SOA and Microservices aim to build modular systems, key differences exist:
-
Size and Granularity:
Microservices are fine-grained, self-contained services with their own databases. SOA, on the other hand, focuses on broader services that are larger and more coarse-grained. -
Decentralization and Independence:
Microservices allow independent development, deployment, and scaling of services, while SOA often involves centralized governance. -
Communication:
Microservices use lightweight RESTful APIs or message queues. SOA typically relies on SOAP and centralized middleware like ESB. -
Scalability:
Microservices excel in scalability due to their fine granularity, while SOA can scale but may require extra coordination. -
Technology Stack:
Microservices adopt a polyglot approach, enabling the use of different tech stacks across services. SOA usually adheres to more standardized technologies.
Why Microservices?
Microservices provide several advantages over monolithic architectures:
-
Scalability:
Each service can be scaled independently based on demand, unlike monoliths, which require scaling the entire application. -
Flexibility:
Microservices can use different technologies, allowing for greater flexibility. -
Independent Deployment:
Services can be deployed independently, reducing downtime. In contrast, monolithic applications require full deployment. -
Improved Fault Isolation:
A failure in one service won’t crash the entire system, unlike monolithic apps. -
Faster Release Cycles:
Microservices facilitate Continuous Integration/Continuous Deployment (CI/CD), allowing faster and safer release cycles.
Principles of Microservices Architecture
- Single Responsibility Principle
- Independence and Decentralization
- API-First Design
- Autonomy
- Decentralized Governance
- Resilience
- Continuous Delivery and Deployment
- Observability
- Loose Coupling
- Data Isolation
- Polyglot Persistence
- DevOps Culture
- Self-Contained and Stateless Services
- Domain-Driven Design (DDD)
Challenges of Microservices
Despite the benefits, microservices introduce complexity in several areas:
-
Increased Complexity:
Managing multiple services, databases, and technologies can complicate development, testing, and deployment. -
Network Latency:
Communication between services can introduce latency and potential points of failure. -
Data Consistency:
Ensuring data consistency across distributed services is challenging, especially when dealing with distributed transactions. -
DevOps Overhead:
Managing microservices requires sophisticated DevOps practices such as automated testing, monitoring, and logging for each service.
Modular Monolith: A Step Toward Microservices
A Modular Monolith structures the system into independent modules within a single codebase. It acts as a middle ground between monolithic and microservices architectures:
-
Single Deployment Unit:
Deployed as one cohesive unit, despite the modular internal structure. -
Clear Module Boundaries:
Modules communicate via well-defined interfaces, simplifying maintenance. -
Easier Evolution:
It allows easier refactoring and future evolution into microservices.
Strangler Fig Pattern
The Strangler Fig Pattern is a strategy for gradually refactoring or replacing a legacy system by introducing a new system alongside the old one. Over time, the old system is phased out.
Key Concepts:
- Incremental Replacement: Gradually move features to the new system.
- Coexistence: The old and new systems coexist, with routing logic deciding where requests go.
- Progressive Decommissioning: The old system is decommissioned as more features move to the new system.
Application Architecture in Microservices
To effectively implement microservices, a well-designed application architecture is crucial. Here are some of the most common architectures used in microservices:
- **Layer Architecture
- **Clean Architecture
- **Verticle Slice Architecture
- **Hexogonal Architecture
Layer Architecture
This architecture divides the application into layers based on their functionality, such as presentation, business logic, and data access. But this approach has challenges like, potential for tight coupling between layers, and
difficulty in scaling individual layers independently.
Clean Architecture
Introduced by Uncle Bob Martin, this architecture focuses on the separation of concerns between the core business logic and the outer layers (presentation, data access, and infrastructure).
Domain Layer → the project that contains the domain layer, including the entities, value objects, and domain services
Application Layer → the project that contains the application layer and implements the application services, DTOs (data transfer objects), and mappers. It should reference the Domain project.
Infrastructure Layer → The project contains the infrastructure layer, including the implementation of data access, logging, email, and other communication mechanisms. It should reference the Application project.
Presentation Layer → The main project contains the presentation layer and implements the ASP.NET Core web API. It should reference the Application and Infrastructure projects.
Vertical Slice Architecture
Vertical Slice Architecture was born from the pain of working with clean architectures. They force you to make changes in many different layers to implement a feature.
This minimize coupling between slices, and maximize coupling in a slice and make the code cohesive for a single use case. All the files for a single use case are grouped inside one folder. This might have potential for duplication of code across slices and might introduce difficulty in maintaining consistency and coherence between slices.
Hexagonal Architecture
Also known as ports and adapters architecture, this approach focuses on the core business logic and its interactions with external dependencies.
This has benefits like, high testability and isolation of the core business logic,
flexibility to adapt to different environments and technologies,
clear separation of concerns between the application and its infrastructure. However, it might introduce high complexity and require careful planning and design.
Enhancing Microservices with DDD and EDD
Domain-Driven Design (DDD)
DDD aims to improve the quality, maintainability, and adaptability of software systems by ensuring that they are closely aligned with the business domain they serve. It is particularly effective for complex domains that require a deep understanding of the underlying business processes.
Strategic Design: Considering the overall architecture and structure of the system to align with the domain.
Tactical Design: Focusing on the implementation details of the domain model, such as entities, value objects, services, aggregates, and repositories.
Event-Driven Architecture (EDA)
Microservices and modern systems often use Event-Driven Architecture (EDA) for asynchronous communication. EDA decouples services, enhancing scalability and performance.
Event Delivery Patterns:
- Pub/Sub: Services subscribe to and publish events.
- Event Streaming: Continuous streams of events are processed by multiple services.
Delivery Semantics:
- At Most Once: Data loss is acceptable, lowest overhead.
- At Least Once: No data loss, but duplicates may occur.
- Exactly Once: Guarantees data delivery without duplication, but with the highest overhead.
Event-Driven Use Cases:
- Fire and Forget: E.g., generating reports.
- Reliable Delivery: E.g., processing online orders.
- Infinite Data Streams: E.g., sensor data for real-time analytics.
Saga Pattern for Distributed Transactions
Microservices often struggle with traditional ACID transactions across distributed systems. The Saga Pattern offers a solution:
- Orchestration Workflow: Centralized control over the transaction sequence. Orchestrator runs the series of operation one after another and in case of failure at any stage it runs the compensation transation in the reverse order to keep the system consistent.
- Choreographing Event-Driven: Each microservice knows its role and triggers the next step.
Happy path:
And in case of failure:
Key Points:
- Loss of Atomicity: Saga compensates by breaking transactions into smaller steps with compensating operations.
- Distributed Transaction: Manages multiple local transactions across different services.
CQRS (Command Query Responsibility Segregation)
The CQRS Pattern separates write (command) and read (query) operations, improving performance and scalability.
Join operation in microservices:
Key Benefits:
- Separation of Concerns: Enhances maintainability by splitting command and query services.
- Performance: Optimizes database operations for reads and writes.
- Scalability: Allows independent scaling of read and write operations.
Event Sourcing
Event Sourcing stores events as a sequence to track changes in the system rather than storing the current state.
Benefits:
- History Preservation: Complete audit trail of events.
- Improved Write Performance: For high concurrency systems.
Challenges:
- Increased Complexity: Requires additional infrastructure and eventual consistency between event logs and the current state.
Conclusion
The evolution from monolithic to microservices architectures has transformed how we build and scale applications. With principles like Domain-Driven Design, patterns like Saga and CQRS, and the power of Event-Driven Architecture, modern systems are more flexible, scalable, and maintainable. However, these advantages come with complexity, requiring careful planning and robust DevOps practices to ensure smooth operation.