B2C microservices are built on heterogeneous tech-stacks (blocking/reactive) as per their traffic and can have common use-cases E.g. Validation, Idempotency. But code can’t be shared/reused due to paradigm contrast. So it’s rewritten everywhere. With a hands-on demo, let’s see how to hasten feature development, by Templatizing code for large & common & well-tested features, to be shared/reused across heterogeneous services, using Open-Source technologies.
Technical Level: Interesting to all, approachable for intermediate and up. Any Functional Programming enthusiasts love it.
This talk targets intermediate to expert senior developers with a good understanding of generics
and some exposure/interest towards blocking and non-blocking/reactive paradigms. This talk is language-agnostic, but I use Kotlin (a Modern Open-source JVM language) in combination with Arrow (A Trending Open-source functional companion for Kotlin).
Kotlin’s syntax is very close to Java, and all software design patterns discussed in this talk can be implemented in almost any language. Thanks to the concise syntax of Kotlin and robust toolset provided by Arrow, implementing this technique turns ergonomic.
I used popular open-source backend frameworks — Spring-MVC
and Spring-WebFlux
to demonstrate heterogeneity, in my POC.
No prior knowledge about these frameworks or kotlin is required, all the nuances required for this problem are contextually explained in the talk. The key takeaways for the audience are:
The trend in the B2C world is to chop the use-cases with varied traffic-needs into Microservices/Macroservices managed by independent Scrum teams. These teams develop using Heterogeneous frameworks and tech-stacks, suitable for the traffic needs of their services.
Reactive/non-blocking stack should only be used for high traffic services, as it adds a lot of complexity to the application. Taking an example from the Payments domain, Purchases tend to have high traffic (especially during Black Fridays, Flash sales, etc.), and it’s common to model them with an Asynchronous non-blocking stack like Spring-WebFlux. Whereas Refunds tend to have relatively low traffic, and a simple blocking stack like Spring-MVC can easily cater to its scaling needs. Such use-cases can be found in many B2C products, E.g. Reservations vs. Cancellations.
Despite being heterogenous, these services have many commonalities in their Domain logic, ranging from small features such as Authentication, Logging, to large features such as Request-Validation, Idempotency, External-Integrations. In the case of homogeneous services, the common code can be placed in a shared module and be added as a dependency in all services. But in heterogeneous case, blocking code can’t be shared/reused for non-blocking service or vice-versa, because:
Effect
it operates on, E.g. Non-blocking paradigms may operate on reactive Effect types like Mono<A>/Flux<A> or Observable<A>
, contrary to blocking paradigms which may (or need not) use simple Effect types like Option/Either
.This leads to scrum teams duplicating the same logic in all the services. Also, a service may be migrated, E.g. from Spring-MVC
to Spring-WebFlux
to scale better for increasing traffic, it needs to be entirely rewritten.
Let’s see (with a working POC) how to make such common logic reusable/sharable, turning the Monomorphic code into Polymorphic templates, which enables scrum teams to share well-tested small & large features across their services.
If the Effect is abstracted out as a Generic, the domain logic turns reusable for service of any type, and it can be called Polymorphic. But to achieve that, we need to understand the concepts - Higher-Kinded Types and Typeclasses.
Effects are of the form F<A>
(e.g. Mono<A>
), where F
is the Effect type and A
is the value type. The problem is, most JVM languages only support parametricity on the value type A
but not on the Container/Effect type F
. So, we need Higher-Kinded Types, to represent F<A>
as Kind<F, A>
.
It’s a generic interface that is parametric on a Type T
. E.g. Comparator<T>
in JDK is a simple typeclass. Comparator<T>
has one operation fun compare(a: T?, b: T?): Int
. Now for a type String
to be a member of this typeclass, prepare a concrete Comparator<String>
implementing its fun compare(a: String?, b: String?): Int
. That’s it! Now the Collections.sort()
can make use of this concrete implementation to compare Strings.
To put our above example into a formal definition - A type class defines some behavior in the form of operations that must be supported by a type. A type can be a member of a typeclass by merely providing implementations of the operations the type must support.
This principle can be used to define abstract interfaces like Comparator<T>
and reusable templates like Collections.sort()
, whose behavior is polymorphic to the type T
being sorted. This is called Ad-hoc Polymorphism. There is a popular Design pattern which uses this technique, known as Template Method pattern.
The term Ad-hoc polymorphism refers to polymorphic functions that can be applied to arguments of different types, but that behave differently depending on the argument type to which they are applied.
The code that relies on type classes is open for extension, just like how Comparator<T>
can be extended to compare any type.
Now that we have both the tools (Higher-Kinded Types and Typeclasses), let’s make a polymorphic template for our reusable domain logic. The samples used in the rest of this paper can be seen in action in a fully working POC
GitHub. It has 3 modules:
kofu-mvc-validation
- Blocking Service built with Spring-WebMVC
kofu-reactive-validation
- Reactive Service built with Spring-WebFlux
validation-templates
- Shared module for both the services, holding templates.
We shall take-up the user validate-and-upsert as our example use-case, where a request to upsert a user is validated, followed by insert or update based on the user existence in the DB.
Spring-WebFlux
works with Mono<A>/Flux<A>
while Spring-WebMVC
doesn’t. As a proof for lack-of reusability problem discussed above, notice how upsert
function is different in both the services, although doing the same functionality
The goal is to abstract this use-case domain logic into a generic reusable template. We shall achieve it by creating some typeclasses and making use of some typeclasses from the Arrow library. These heterogeneous services can inflate and consume these templates by supplying concrete instances of those typeclass interfaces. I coined this technique as Template-Oriented-Programming!
Repo<F>
typeclassLet’s abstract the DB behavior in both these stacks to a generic typeclass interface, Repo<F>
, where F
represents the Effect-type on which the DB works in their respective stacks/paradigms.
interface Repo<F> : Async<F> {
fun User.update(): Kind<F, Unit>
fun User.insert(): Kind<F, Unit>
fun User.doesUserLoginExist(): Kind<F, Boolean>
fun User.isUserCityValid(): Kind<F, Boolean>
}
These operations have a return type of the form Kind<F, A>
(=F<A>
), which is generic and agnostic of the Effect
.
The Repo<F>
inherits from Async<F>
, which is a typeclass from Arrow Library to represent Effectful Operations.
Our services implement Repo<F>
typeclass with their respective Effect types.
In these concrete implementations, IO
and MonoK
supply concrete instances for Async<F>
, and the service repository functions are mapped to Repo
operations
Now we can weave our business-logic into generic templates depending on the generic operations of the typeclass Repo<F>
. Templates are generic functions and they depend on Typeclasses. This dependency can be achieved by passing typeclass as a function argument or declaring the template functions as extensions to a typeclass. I used the latter in my POC
Ref . This file has all the validation rules for a user. The typeclass here is EffectValidator<F, S, ValidationError>
, which in-turn is composed of two typeclasses ValidatorAE<S, E>
(abstracts Validation Strategies) and Repo<F>
( Discussed above). The generics in these typeclasses stand for:
F: Effect type - Used by Repo<F>
to signify the effect in which the DB operates.
S: Strategy type - Used to decide the strategy in which validations should run (e.g. Fail-Fast or Error-Accumulation).
However, these User-rules are generic functions aka Templates, which are agnostic of validation orchestration strategy (S
) and the paradigm Effect (F
) in which these are triggered (blocking/reactive). This can also be seen from the return types of these functions - Kind<F, Kind<S, Boolean>>
.
To consume these templates, the EffectValidator<F, S, ValidationError>
typeclass acts as the bridge between services and templates.
The concrete implementations of the typeclass supplied by Services, essentially fill in the blanks for the templates.
These templates work as shared logic, and the services can use those concrete instances to consume all these templates.
Refer how both the services are able to seamlessly call the validation templates using the concrete instances without rewriting the rules and orchestration:
Sharing code among micro-services is seen as an anti-pattern as it causes tight-coupling and eager-abstractions. But, this is specific to Parallel services like Payments-Refunds, Reservations-Cancellations etc., which share a lot of domain logic and is not bound to change. Plus, this is sharing at granular level. As discussed above, these templates are Extensions of typeclass and services (consumers) are the ones, which breathe life into them. That means, any new service or service migration can pick and borrow and extend at granular level, all those well-tested small and large features for free with minor efforts! Moreover, typeclasses are entirely extensible to support more operations, in turn, to extend and expand our template base.
⚠️ A word of caution, this technique should NOT be misused on the name of DRY to abstract duplication everywhere. Always, give the duplicate code some time to settle and evolve. 👯♂️
We achieved reusable domain logic using Ad-hoc Polymorphism, abstracting out the Effect using Typeclasses and Higher-Kinded Types, turing our Monomorphic code to Polymorphic. This is very powerful to model and migrate Parallel B2C-services. This zeros-down the cost and effort to rewrite and maintain common business logic across all services and service migrations. This can save a release cycle amount of work, increases project stability, bringing in real agility among scrum teams and startups to ship features faster. All of this is achieved with Free & Open-Source technologies.
The idea was presented at Kotlin User Group, Hyderabad https://www.meetup.com/en-AU/kotlinhyderabad/events/269763753/ (This is only an intro to explain prerequisites for the talk in this post.)
The Slide deck
https://www.intuit.com/blog/uncategorized/kotlin-development-plan/
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html
https://blog.pragmatists.com/unobvious-traps-of-spring-webflux-16924a0d76d5
https://danielwestheide.com/blog/the-neophytes-guide-to-scala-part-12-type-classes/
https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf
https://people.csail.mit.edu/dnj/teaching/6898/papers/wadler88.pdf