Building Composable Microservices with Module Federation
While microservices speed time-to-market through focused, autonomous teams, shorter development cycles and deployment independence; they are considerably more complex to build and maintain than traditional monoliths. So much so, that many projects underdeliver or fail outright.
Module federation offers a solution to this complexity by allowing applications to be developed and integrated like monoliths, while preserving the benefits of autonomy and loose-coupling that make microservices so attractive. Using module federation (a la Zack Jackson) and extending the ideas behind clean architecture to the components of, and relations between, microservices, we can develop federated components in ways that simplify service deployment, integration, and orchestration.
Hexagonal or port-adapter architecture is a clean software architecture pattern that insulates or decouples an application’s domain logic from the rest of the codebase by controlling domain I/O through a set of durable interfaces, known as ports. The application layer then implements one or more adapters for each port. This method of dependency inversion, like IoC generally, minimizes the impact that changes to infrastructure, external services, or third party libraries can have on the application － thus enabling the application to be more easily extended and maintained over time. Module federation allows us to apply these same ideas in new ways to solve some of the more intractable issues we encounter when building and managing microservices.
Hexagonal software architecture with federated modules
If module federation threatens to revolutionize Micro-Front Ends, then the same can be said for the backend. With the introduction of module federation, we can import remote modules just as if they were installed locally. But unlike local libraries, they are not a build-time dependency. They can be managed in a separate repo, developed by a separate team, and deployed independently of the applications that import them. The ability to import code remotely and dynamically like this not only answers the persistent question of how to share code in a microservice-style architecture, but also affords several new architectural choices when designing applications.
Independent services imported into a common host framework
As we saw above, in a hex architecture, ports and adapters control I/O between the application core (domain) and the outside world. Using module federation, we can expose client libraries and service adapters to be imported remotely at runtime and bound dynamically to ports exposed by the domain. Integrating microservices becomes a matter of calling a local library. This is not dissimilar from Uncle Bob’s notion of microservices as independently deployable libraries, instead of distributed executables talking over a network — the key difference being we don’t need to install federated libraries.
Returning to the components of our architecture, if we can federate service adapters, why not federate the service itself? With a minimal set of interfaces, we can do just that. Using a simple host framework, we can import a domain model and dynamically generate a set of endpoints to expose it via REST, GraphQL, etc.
Federated service and adapter modules imported into host framework
Consider the same framework instance importing multiple services. Now services can interact with one another locally (e.g. via in-memory function calls or local framework events) to boost performance, reduce footprint and simplify operations.
They can also share a federated schema, like we find in GraphQL, which allows relationships to be defined transparently between entities without creating dependencies. But unlike GraphQL, queries can execute locally against an in-memory datasource. This is not to say they share a common database. Whether collocated or not, services retain their deployment and data independence.
Multiple services hosted in a single process
When it comes to orchestrating or choreographing services to achieve a business result, ports can be piped together to form control flows by configuring the output event of one port as the input or triggering event of another. This works equally well whether ports are local or remote. To handle errors and roll back transactions, the history of port invocations can be recorded, so compensating workflow can be generated dynamically. While this solution is event-driven and distributable across multiple service instances, traits typical of choreography, any service can act as an “orchestrator” and manage the process from start to finish.
Configuration-based service orchestration
Proof of Concept
As a proof of concept, this simple REST API framework supports CRUD operations for domain models whose source code, and that of any dependencies, is streamed over HTTP from a remote server at runtime, using module federation. Following hexagonal architecture, the framework can be configured to generate ports and bind them dynamically to local or federated adapters, as well as compose them into workflows.
The sample code in MicroLib-OrderService shows a federated domain object, Order, whose ports are bound to a set of mock services, which are used locally for testing. When imported by the host framework, the ports are rewired to the “real” service adapters/clients, which talk to the actual service endpoints. Additionally, its ports are piped together into a workflow, or what is sometimes called an “orchestration-based saga”, that controls an order process involving a payment, inventory, and shipping service.
For an in-depth look at the PoC, see: Stop Paying the “Microservice Premium” Achieving Deployment Independence in a Monolithic Architecturetrmidboe.medium.com