- The Ball Of Mud represents the worst case scenario for a Monolith. Also called as Spaghetti Code.
- No clear isolation in the application.
- Complex dependencies where everything depends on every other thing.
- Hard to understand and harder to modify.
- To clean up the ball of mud we introduce isolation into the application by dividing the application along clear domain boundaries.
- We introduce packages and libraries that help isolate related pieces of code. They provide a clean and consistent interface.
- Deployed as a single unit.
- Single shared database.
- Communicate with synchronous method calls.
- "Big Bang" style releases i.e., "All or Nothing" leading to longer release cycle times usually from weeks to months.
- Requires teams to carefully synchronize features and releases.
- Multiple copies of the monolith are deployed.
- Each copy is independent. They don't communicate with each other.
- The database provides consistency between deployed instances. In other words, communication between instances of a monolith typically happens through the database.
- Easy cross module refactoring
- Easier to maintain consistency
- Single deployment process
- Single thing to monitor
- Simple scalability model
- Limited by the maximum size of a single physical machine.
- Only scales as far as the database will allow.
- Components must be scaled as group even if their is no such scalability requirement for a particular component.
- Deep coupling leads to inflexibility in terms of necessary refactorings.
- Development is typically slow (change is difficult, built times long).
- Serious failure in one component often brings down the whole monolith.
- Redistribution of load can cause cascading failure.
- Besides using packages or libraries, we can introduce additional isolation into our Monolith using Service Oriented Architecture.
- Services don't share database.
- All access must go through the API exposed by the service.
- SOA doesn't dictate any requirements around deployment. Service may live in the same process (monolith) or may be hosted as different applications (microservices).
- Microservices are a subset of Service Oriented Architecture.
- Logical components are separated into isolated microservices.
- Microservices can be physically separated and independently deployed.
- Each component/microservice has its own independent data store.
- Microservices are independent and self governing.
- Each service is deployed independently.
- Multiple independent databases.
- Communication is synchronous or asynchronous (Reactive Microservice).
- Loose coupling between components.
- Rapid deployments (possibly continuous).
- Team release features when they are ready.
- Team often organized around a DevOps approach.
- Each microservice is scaled independently.
- Could be one or more copies of a service per machine.
- Each machine hosts a subset of the entire system.
- Individual services can be deployed/scaled as needed.
- Increased availability. Serious failures are isolated to a single service though cascading failures due load redistribution is still possible.
- Isolation/decoupling provides more flexibility to evolve within a module.
- Supports multiple platform and languages.
- Microservices often come with organizational change.
- Teams operate more independently.
- Release cycles are shorter.
- Cross team coordination becomes less necessary.
- These changes can facilitate an increase in productivity.
- May require multiple complex deployment and monitoring approaches.
- Cross service refactoring is more challenging.
- Requires supporting old API versions.
- Organizational change to microservices may be challenging.
- The Single Responsibility Principle (SRP) applies to object oriented classes, but works for Microservices as well.
- A microservice should have a single responsibility (eg. managing accounts).
- A change to the internals of one microservice should not necessitate a change to another microservice.
- Bounded Contexts are a good place to start building microservices. They define a context in which a specific model applies. Although further subdivision of the Bounded Context is possible.
- As we move from Monoliths to Microservices we are introducing more isolation.
- Isolation provides reduced coupling and increased scalability.
- Reactive Microservices are isolated in: State, Space, Time, Failure
- All access to a Microservice's state must go through its API.
- No backdoor access via the database.
- Allows the microservices to evolve internally without effecting the outside.
- Microservices should not care where other microservices are deployed.
- It should be possible to move a microservice to another machine, possibly in a different data center without issue.
- Allows the microservices to be scaled up/down to meet demand.
- Microservices should not wait for each other. Requests are asynchronous and non-blocking. This results in more efficient use of resources. Resources can be freed immediately, rather than waiting for a request to finish.
- Between microservices we expect Eventual Consistency. Provides increased scalability. Total Consistency requires central coordination which limits scalability.
- Reactive Microservices also isolate failure.
- A failure in one Microservice should not cause another to fail.
- Allows system to remain operational in spite of failure.
- Bulkheading is a tool used to isolate failure.
- Failures are isolated in failure zones.
- Failures in one service will not propagate to other services.
- Overall system can remain operational (possibly in a degraded state).
- Introducing microservices has given us a form of bulkheading since each microservice can act as a potential failure zone.
What happens when a service depends on another that is overloaded?
-
Calls to the overloaded service may fail.
-
The caller may not realize the service is under stress and may retry.
-
The retry makes the load worse.
-
Callers need to be careful to avoid this.
-
Circuit Breakers are a way to avoid overloading a service.
-
They quarantine a failing service so it can fail fast.
-
Allows the failing service time to recover without overloading it.
-
Akka and Lagom both feature circuit breakers.
-
A circuit breaker between the service and the database would allow us to prevent the database from becoming overloaded. It also allows us to fail fast when the database is slow to respond.
- Async, non-blocking messaging allows us to decouple both Time and Failure.
- Services are not dependant on the response from each other.
- If a request to a service fails, the failure won't propagate.
- The client service isn't waiting for a response. It can continue to operate normally.
- We build systems using principles of isolation to achieve what we call as Autonomy.
- Autonomy is basically the idea that each of our services that we build can operate independant of one another.
- Microservices can only guarantee their own behaviour (via their APIs) and not that of their peer services.
- Isolation allows a service to operate independent of other services.
- Each service can be Autonomous.
- Autonomous services have enough information to resolve conflicts and repair failures.
- They don't require other services to be operational all the time.
- Autonomy allows for stronger scalability and availability.
- Fully autonomous services i.e, services that have no external dependencies whatsoever can be scaled indefinitely. Fully autonomous services however isn't something very practical but the closer we can get a system to being fully autonomous, the better it would be.
- Operating independently means that they can tolerate any amount of failure.
- Communicate only through asynchronous messages.
- Maintain enough internal state for the microservice to function in isolation. This may require Eventual Consistency to be in place. Having an eventually consistent cache of the delivery data in the driver's app would be unnacceptable because most changes would not be allowed after the order has left the restaurant.
- Avoid direct, synchronous dependencies on external services.
- Microservices can lead to complexities in the API.
- A single request may require information from multiple microservices.
- Client in such an architecuture are required to handle the complexity of sending out many requests to multiple microservices and aggregating the results.
- Mostly updating client code is in control of user who may choose not to update the client as frenquently as the services are getting updated. This leads to slowed development and improvements.
- Requests can be sent through a Gateway Service.
- A Gateway Service sends the requests to individual microservices and aggregates the response.
- Logic for aggregation is moved out of the client and into the Gateway Service.
- Gateway handles failures from each service. Client only needs to deal with possible failure from the gateway.
- Gateway services creates an additional layer of isolation between our client and the individual microservices. More isolation, generally is a good thing.