A microservice (aka service) is a unit of functionality that can be deployed and scaled independently.
In a microservice architecture, a variant of service-oriented architecture (SOA), services communicate between each other via HTTP, Web Sockets, or a messaging protocol such as AMQP.
Going over the sign-in example in the event-driven architecture blog post we could refactor it into a simpler service that does the following:
- Receive HTTP request with user credentials.
- Read user from database to validate credentials.
- Emit UserEvent.SignedIn (or no event if credentials are invalid).
- Return HTTP response.
The decoupling of functionality allows us to extract it into a new service making it more scalable and by publishing UserEvent.SignedIn events, we can make it easy to plugin-in new features based on such event, making the system pluggable.
Implementing Event-driven architecture and microservices allows us to decouple the services and gives us scalability, fault-tolerance and observability.
Indecent services can be scaled out independently (aka horizontal scaling) on demand. When we split the responsibilities across unique small services, we can scale a specific functionality independently, which can also be cost-effective when running in the cloud.
Microservices also enable fault-tolerance, as our system might be capable of continuing to serve requests in the presence of failures. Regarding our users’ sign-in example, if the service responsible for increasing the number of users online has a temporary network issue, the system will still operate normally. The UI might show the wrong number of users for a moment, but it will eventually be right once the service is up again. This property is known as eventual consistency.
An event-driven architecture can also be highly observable. If all the events in our system flow throughout a central event channel, we can inspect each of them. This enables auditability of the system, which is usually a requirement in any banking or financial service.
Sometimes we may need to recreate the application’s state from events, also for this purpose. This is all possible in an EDA system without affecting other services.
The Four Decisions in Microservices Architecture
Let’s say now after you read about the microservices architecture you want to realize it, The following challenges are important to consider:
Handling No Shared Database(s)
Each microservice should maintain its own databases, avoiding shared databases to prevent tight coupling and potential breakages from schema changes. While this rule is sound, challenges arise when two services need transactional updates on shared data. Two solutions are:
- Use asynchronous messaging when only one microservice updates the data.
- For mutual updates, either merge the services or use transactions.
When handling data consistency:
- Whenever feasible, localize updates within one microservice, although this might risk creating monolithic structures.
- Implement compensation and consider lesser guarantees, like eventual consistency or manual user updates. For instance, if a process fails, reverse its preceding steps, like refunding money if a shipment fails. There are situations, however, where precise transactions are crucial. Always assess the benefits and risks before deciding on an approach.
Handling Microservice Security
The traditional method involves a service authenticating via a database or Identity Server upon receiving a request. While you can substitute the identity server with a microservice, this can complicate dependency structures.
A more efficient alternative is the token-based approach as outlined in “Building Microservices.” Here, the client or gateway communicates with an identity/SSO server. This server authenticates the user, then issues a token detailing the user’s identity and roles (using mechanisms like SAML or OpenIDConnect). Microservices then validate this token and authorize actions based on the roles within. For instance, a “publisher” might get different query results compared to an “admin” due to varied permissions.
Handling Microservice Composition
Here, “composition” means “how can connect multiple microservices into one flow to deliver what end-user needs”.
Avoiding Dependency Hell
Microservices allow each service to release and deploy independently, but avoiding dependency issues is crucial. Consider two cases with microservices “A” transitioning from API “A1” to “A2”:
- Microservice B might use A1-based messages for A2 (backward compatibility).
- If Microservice A reverts to A1, Microservice C might keep sending A2-intended messages.
Handling these cases often involves adding optional parameters and preserving existing ones. More intricate situations can arise. For a deeper understanding, refer to “Taming Dependency Hell” with Michael Bryzek and “Ask HN: How do you version control your microservices?”.
Support for backward and forward compatibility should have a time limit, perhaps not relying on APIs older than three months, letting developers eventually streamline their code.
Concerning microservice dependencies, two models exist:
- Directly invoking microservices as needed, leading to a tangled architecture.
- A tree structure, with microservices interacting only through an API gateway or message bus, centralizing most business logic at the gateway.
The best approach is either fully embracing the orchestration model or implementing choreography diligently to avoid an unmanageable architecture.