The advantages of microservices can generally be categorized into two groups: technological and human-centric. Within the human-centric category, the benefits relate to how we work and collaborate.
Teams working on microservices have full ownership over parts of the system. They can work independently, choose their own stack, and deploy their services independently and in parallel to other teams.
In addition to that, although microservices as a system can become more complex, individual services are simpler and easier to understand and maintain.
This sort of “reverse Conway’s Law,” where the system’s architecture influences the organizational structure, is a key benefit of microservices that can also be found in other types of distributed systems.
How can we make our system more distributed and composable to get more of the same benefits?
In this article, we’ll look at Bit as a tool that allows us to compose systems from independent components of all levels of granularity, from simple utility functions to microservices and, ultimately, to entire systems.
Bit: A next-generation build system for composable software.
We’ll see how Bit components can be shared and reused across microservices, how microservices can be maintained as Bit components, and how they can be composed in runtime using an API gateway component.
Building a composable system
To better understand how Bit can be used to compose microservices, let’s start by creating a new workspace with a few Bit components:
bit new react my-project --env bitdev.react/react-env --default-scope my-org.my-project
cd my-project
Your new workspace has seven components. Run bit start
to explore your components using Bit’s UI:
To run the system, run:
bit run my-project
The output should list several different processes listening on different ports. Two microservices, discussion-server
and user-server
, an API gateway, and a frontend application:
[discussions-server]: running on port 5003
[user-server]: running on port 5002
[HPM] Proxy created: / -> http://localhost:5002/graphql
[HPM] Proxy rewrite rule created: "/user-server/" ~> "/"
[HPM] Proxy created: / -> http://localhost:5003/graphql
[HPM] Proxy rewrite rule created: "/discussions-server/" ~> "/"
backend server running on 5001
frontend server running on 3001
This runtime composition of different services, both locally and remotely, is done using the platform
component. It is another Bit component whose role is to serve as the entry point for this system:
import { Platform } from '@bitdev/platforms.platform';
const UserServer = import.meta.resolve('@bitdev/node.examples.user-server');
const DiscussionServer = import.meta.resolve('@my-org/discussions.discussions-server');
const AcmeWeb = import.meta.resolve('@my-org/my-project.my-platform-project-web');
const PlatformGateway = import.meta.resolve('@bitdev/symphony.backends.gateway-server');
export const MyPlatformProjectPlatform = Platform.from({
name: 'my-platform-project-platform',
frontends: {
/** main frontend application for the platform */
main: AcmeWeb,
},
backends: {
/**
* use an api gateway component.
* supports proxy of graphql and restful requests.
*/
main: PlatformGateway,
services: [
/** compose micro-service components. */
UserServer,
DiscussionServer
]
},
});
Note that not all Bit components need to be maintained locally. A composition of services can include remote services that are not available locally. This is tremendously helpful for local and remote testing of services in the context of the entire system.
To learn more, see the Platforms documentation.
Also, note that the API Gateway can be implemented in whatever way suits your needs. To explore the one used by this example, see:
Build-time or runtime integration? The choice is yours!
One thing that is immediately apparent when reviewing the Bit components in your workspace is just how different they can be. UI and non-UI, Node and React, small and large.
However, in the context of this article, the most important distinction is whether they are “app components” or not. App components are components that are available for consumption as Node packages, just like “regular” Bit components, but are also deployable and available for runtime compositions.
For example, our discussion-server
Bit component runs as a separate process. Looking at its implementation, we can find one file that sets it apart from other components: the .bit-app.ts
file.
/** @filename: discussion-server.bit-app.ts */
import { NodeApp } from '@bitdev/node.node-app';
export default NodeApp.from({
name: 'discussions-server',
artifactName: 'discussion-service',
mainPath: import.meta.resolve('./discussions-server.app-root.js'),
/**
* an optional deploy function for this app component -
* to be executed when a new version of this compoennt is released
*/
// deploy: () => {}
});
With Bit components, you’re always building and delivering independently.
You can enjoy the benefits of a composable and distributable system, regardless of how (and when) components are being integrated. This gives you the freedom to construct a system that perfectly balances your human or organizational needs with technological requirements.
For example, you might want more than one team to maintain different aspects of a microservice. You can split the microservice into several microservices, but that’s not always the best solution when taking into consideration your system’s performance. It might be that build-time integration of independently delivered Bit componetns is a better choice.
A symmetry between dependencies in dev and prod
One benefit of this style of composition of Bit components is that it provides you with a clear dependency graph that includes components composed in runtime. This can be tremendously helpful in understanding complex systems and in maintaining them.
As mentioned earlier, with Bit your system’s composition is a hybrid of services integrated in runtime and packages integrated in build time. Zooming out a bit further will reveal some of the Bit components used to compose this services:
Reusing Bit components: faster development and better standardization
Bit components can be maintained, released, and installed from any project. That makes them an excellent tool for reusing code and standardizing the development process across separate projects.
The ‘discussion’ entity component as an example
For example, your workspace has an entity component called discussion
. The discussion
component provides the data modeling and a set of operations that can be utilized by any microservice or frontend application that handles discussion data within your system.
It is a contract between services that outlines the structure and rules of engagement for discussions, ensuring consistency and reducing the duplication of effort when developing new features that interact with discussions.
Furthermore, by using Bit’s versioning system, developers can easily upgrade to new versions of the discussion component as it evolves while maintaining compatibility across the services that depend on it.