Reactive Microservices — 8
This is an 8 part series on reactive microservices. [1 , 2 , 3, 4, 5, 6, 7, 8]
Part 2 , 3 , 4 , 5 , 6 & 7 of this services listed reactive microservices patterns. This part continues to list more patterns.
Reactive Anti-Patterns
Shared Mutable State
When adopting Microservices, it is essential to eliminate shared mutable state and thereby minimize coordination, contention and coherency cost, as defined in the Universal Scalability Law by embracing a Share-Nothing Architecture. Shared mutable state arises out of both concurrent thread execution or a design that attempts integration between services via a Database by employing DB locks. The state should be responsibility of a single microservice (an aggregate).
https://www.reddit.com/r/announcements/comments/4y0m56/why_reddit_was_down_on_aug_11/
Synchronous Thread Blocking Computation
Any part of system that introduces a thread-blocking processing, would introduce contention in the system and lead to thread starvation, affect latency and availability. This is true for UI layer as well. Usage of Future, Streams and reactive frameworks like RxJava and Akka provides tools to avoid thread blocking. This can also be avoided by upfront design. For example, instead of having to make a synchronous call over the network to fetch data that is needed within an aggregate, an event notification that notifies the consuming aggregate of a state change in the source aggregate which allows consuming aggregate to store a read-only version of the state data to make local DB or in-memory calls to that data.
Consistency boundaries span multiple microservices
Following DDD provides consistency within a bounded context. Design that leaks the consistency into other contexts leads to loss of domain purity.
Version-less APIs and Events
Interoperability is a key trait between parts of system that evolve and make changes to their behavior via new models for APIs and Events. Versioning both and publishing the support for previous versions and deprecating the versions allows flexibity of change.
It is also important to use event formats that are flexible and can adapt to changes to event schema (ref: https://github.com/iluwatar/java-design-patterns/tree/master/tolerant-reader)
Protobuf(https://developers.google.com/protocol-buffers/) and JSONs are good candidates for events. Serialized objects are inflexible and should not be used.
Ref: https://www.infoq.com/news/2017/07/versioning-event-sourcing/
Assuming Message Ordering and Delivery
The uncertainty of the distributed systems means that there no guarantees of message delivery and that a message may be delivered out of order (especially in case of failures or retries), may be delivered twice by the message broker or may get lost due to network failures. These scenarios should be modelled in the aggregate and handled explicitly. The design should thus me commutative (https://en.wikipedia.org/wiki/Commutative_property) and Idempotent(https://en.wikipedia.org/wiki/Idempotence)
No Timeouts
The system should be explicit about receiving a response from a service. This could be either within a microservice, where a step expects a response within a certain timeframe or from a browser AJAX call that freezes a part of UI (by showing the wait icon). These timeouts should be defined as part of business NFRs. Of course, having asynchronous event pub-sub model in service eliminates the wait times, but there are parts of system that do need to depend on responses, these parts should be explicit about how long they are willing to wait.
A message timeout may mean these things, and should all be accounted for:
- Request has failed
- Request succeeded but response failed
- Request is queued and would be processed eventually.
These are especially tricky for Sagas.
Distributed Transactions
This is obvious by now that expecting strong consistent, 2PC type global transaction hides the uncertainty of distributed components. Transaction boundaries should be within an aggregate and any co-ordination should be via events and compensating transactions. The user experience should be explicit about asynchronous distributed transactions.
No Scaling In
Cloud platforms are efficient in scaling the workloads in case of utilization or failure. Equally important is scaling-in when the spikes or failure event has passed so that the system doesn’t remain perpetually over provisioned. Predictive autoscaling or rule based scaling should be able to scale in both directions.
This is an 8 part series on reactive microservices. [1 , 2 , 3, 4, 5, 6, 7, 8]