Designing Composable Services
Use composable services to optimize application design tradeoffs
Experienced software designers know that successful application systems are created by carefully choosing the best compromises among competing requirements. Composable services can dramatically reduce the kinds of compromises we need to make.
To design applications that take advantage of composable services we need to think outside the box — or at least learn the dimensions of the bigger box within which composable services are bounded.
Appendix A: Textbook Definition of Composable Services describes the software engineering purpose and requirements of composable services. It also warns of the complexities of composable services that must be addressed. As you read this article, we hope you will find that we have clearly shown how that complexity can be effectively managed.
What Does Composability Do for Us?
An application system’s requirements are often too varied or too complex to fit comfortably within the limitations of any one architectural pattern— be it layered, event-driven, plugin, microservices, or cloud.
Composable services simultaneously support synchronous and asynchronous messaging and pub-sub events — all while being deployable both on-premises and in the cloud — and with exactly the same self-configuring and performant component source code.
The composable service pattern focuses on optimizing three fundamental attributes of software components — how components are bounded, how components are deployed, and how components connect to each other. Except for using a network and a JVM, composable services have very few biases concerning application functionality or the technologies used.
What Makes a Composable Service Different?
On its surface, the composable services pattern is an extension of the microservices pattern. Five specific attributes of the composable services pattern, in combination, turn it into a much more capable and powerful concept:
- A composable service is discoverable,
- a composable service is a reactive message processor,
- a composable service is independently deployable,
- a composable service operates within a domain-bounded context, and
- composable services use a single service class per persistent entity.
Composable Services Are Discoverable
When a composable service is placed in the classpath of a message orchestrator instance, it is automatically detected by that message orchestrator. When a composable service is then queried by the orchestrator, it gives the orchestrator all the information necessary to communicate with it.
If the composable service subscribes to an event topic, the orchestrator subscribes to the topic queue and polls it on behalf of the service. Incoming events are forwarded to the subscribing service using the appropriate ComposableService interface method.
Message orchestrators are self-configuring and federated. That means that they communicate with each other — so that every orchestrator on the network knows the details about the services registered with all the other orchestrators.
There is an active message orchestrator on each networked JVM that makes up the application system — either on a Jakarta EE server or in a cloud container.
It is the orchestrator’s job to deliver messages to the optimum instance of the target service by the most effective means available — via a protocol like HTTPS or WebSockets, a high-speed message queue like Kafka or ActiveMQ Artemis — or as a direct method call if there is an instance of the target service within the same JVM.
Orchestrators maintain performance statistics on individual service instances. Those statistics are continually updated and weighted in favor of the most recent activity. The orchestrator uses those statistics when choosing (among all the available service instances of that class and version) the specific service instance to which to send the message.
Service discoverability enables message orchestrators to automatically configure themselves based on the available service instances across the network. It also enables orchestrators to implement service failover, load balancing, and observability.
Composable Services Are Reactive Message Processors
A composable service is stateless and reacts to a individual request or event message using a single thread. It is fully reentrant so that it can process multiple requests or events concurrently.
All the information required by the service to process a message must be present in the incoming message or in a persistent data store. To be fully reentrant, composable services have only method-level variables.
Preconditions and postconditions are enforced when messages are sent or received. Pre and post conditions validate and filter the data in the messages that services receive and send. Preconditions are asserted for incoming messages and postconditions for outgoing messages. Pre and post conditions are declarative. They are declared for data elements, data structures, entities, and messages.
The only public methods a composable service implements are those in the ComposableService interface (see Appendix C: ComposableService Interface Methods). Any other methods have a private scope so that they cannot be accessed from outside the class.
Messages are self-describing and do not require the added complexity of a separate schema management system. Federated message orchestrators can seamlessly locate and deliver requests and events to registered composable services anywhere on the network.
Composable Services Are Independently Deployable
Composable services are deployed in JAR files simply by adding the JAR file to the classpath of a message orchestrator instance. They and the message orchestrator are self-configuring.
If a service depends upon any additional Java libraries, those libraries must be in the orchestrator’s class path.
A composable service can access any other composable service upon which it depends, as long as it has been deployed to at least one of the federated orchestrators on the network.
Not only can service classes be deployed independently, but different versions of the same composable service class can be deployed concurrently on the network.
A message orchestrator will route a request or event to the most recent version of a service method compatible with the version identified by the requester. This can make deploying and managing versions, enhancements, and fixes almost seamless.
The observability of composable service activity, as implemented by message orchestrators, makes visible and actionable the details of any deployment deficiencies — such as unavailable services and versions, insufficient instances of specific services, or other performance bottlenecks.
Composable Services Implement a Domain-Bounded Context
Composable services divide a system into clear domain scopes to create a modular architecture that is easier to understand, develop, and maintain. Each service focuses on a specific set of functionalities or business capabilities, making it easier to reason about and modify independently.
Services with well-defined domain scopes can be deployed independently, with less chance of impacting other services in the system. This creates more flexible and agile deployment processes. Individual service additions, deletions, updates, or version dependency changes can be made without requiring full system deployment.
Each service has a clear ownership and responsibility for a specific domain or business capability. This promotes accountability and clear delineation of responsibilities among development teams. It allows teams to focus on their specific areas of expertise and make decisions independently.
A complex domain can be broken into atomic, reusable, and manageable sub-domains that can be aggregated to implement more complex domains. This is especially important when implementing domains that act upon multiple persistent data entities.
Composable Services Use a Single Service Class Per Persistent Entity
Access to persistent data entities is through a single composable service class dedicated to the individual entity class. A single service class per data entity class enhances code organization, reusability, maintainability, and testability.
A dedicated service class for each persistent data entity type makes the codebase more organized and structured. All the operations related to a specific entity class are encapsulated within the class, making it easier to locate and maintain the relevant code.
Having a single service class for a data entity type enhances the testability of the system. Since the service class encapsulates all the operations related to the entity, it can be more easily isolated for unit testing.
A persistent entity can represent a database entity, an electronic device, or a façade interfacing with legacy or external APIs.
Aggregate component services can be created from multiple persistent entity services to implement more complicated domain contexts.
The aggregate root ensures consistency and maintains the referential integrity of the aggregate while enforcing business rules and invariants. All interactions with the components within the aggregate are performed by messaging the aggregate root.
So … What Does All That Mean for Us?
Software development is a process of discovery, learning, and experimentation that requires a highly iterative process of trial and adjustment that continues for the life of the application. It requires agility — which, in turn, requires architectural models that minimize the cost of iteration. The composable service pattern is just such a model.
Message orchestration coupled with self-contained independently-deployable components — that are reactive processors of both request and event messages — is a nearly ideal model for highly iterative and performant networked applications that can be simultaneously deployed on-premises and to the cloud.
If you found this article useful, a clap would let us know that we’re on the right track.
Thanks!
Recommended Reading
If you think that the composable services pattern might offer solutions to some of your application requirements, here is some suggested reading:
- Is OOP Relevant Today?, does object-oriented programming still have a purpose?
- Building with Composable Services, provides a more formal and detailed description of the composable services architectural pattern.
- Composable Services, describes what makes a service composable.
- The Magic of Message Orchestration, introduces the wiring that connects composable components.
- Building Software Systems, breaking through the complexity of developing software.
- Designing a REST API, why transferring state representations beats remote procedure calls.
Appendix A: Textbook Definition of Composable Services
Composable services in software refer to those that are designed in a way that they can be easily combined to form more complex systems or applications. These services typically have well-defined interfaces and functionality, and are designed to interact with other services in a loosely coupled manner. This is a core principle in microservices architecture and is often related to concepts such as modularity and interoperability.
A composable service should exhibit the following characteristics:
- Loosely Coupled: The service should be designed in such a way that it is independent and can be deployed, updated, and scaled without impacting other services in the system.
- Well-Defined Interfaces: The service should provide a well-defined interface (API) that other services can use to interact with it. This interface should be consistent and maintainable, allowing other services to reliably use it over time.
- Single Responsibility: In line with the principle of single responsibility, each composable service should do one thing and do it well. This makes the service easier to understand, develop, and maintain, and makes it more reusable in different contexts.
- Interoperability: The service should be designed in a way that it can interact with other services, regardless of the specific technologies those services may use. This often involves using standard protocols and data formats.
Composable services are a fundamental building block in modern software architectures, especially in the context of microservices and serverless computing. By breaking down a complex system into composable services, developers can achieve greater flexibility, scalability, and resilience, and can more easily update parts of the system without affecting others.
However, managing a system composed of many different services also introduces complexities around data consistency, service orchestration, and system monitoring that need to be carefully managed.
<back>
Appendix B: Composable Service Messaging API
Composable services send or post messages and publish events through their message orchestrator using these methods. Messages and events are delivered to the target service as ComposableService interface method calls.
Messages that are sent over a network or published to a message queue are serialized by the message broker and subsequently deserialized for delivery to the target service.
- public CSResponse sendMessage(String method, String serviceName, String serviceVersion, Object… parms) :: Synchronously send a message that expects a response — through the message orchestrator to a service. Parms vary depending on the method.
- public CSResponse postMessage(String method, String serviceName, String serviceVersion, Object… parms) :: Asynchronously post a message that expects only an orchestrator receipt confirmation — through the message orchestrator to a service. Parms vary depending on the method. Posting a GET message will throw an error.
- public CSResponse publishEvent(String method, String topicName, String version, Object… parms) :: Publish a message through the message orchestrator to a message queue. Parms vary depending on the method. Publishing a GET event will throw an error.
In the context of a composable service. there is no difference in the handling of a request or event. If a composable service subscribes to an event queue, the orchestrator subscribes to the queue topic and polls it on behalf of the service. Incoming events are forwarded to the subscribing service using the appropriate ComposableService interface methods as a request message.
Appendix C: ComposableService Interface Methods
Composable services implement the ComposableService Java interface in order to be discovered, registered, and accessed by a message orchestrator.
The methods of the ComposableService interface are deliberately RESTful. RESTful APIs use a uniform set of well-defined operations, GET, POST, PATCH, PUT, and DELETE to transfer the state of an application context between requestors/publishers and services.
This uniformity allows developers to learn and use the APIs more easily — to simplify and standardize the shape of services and the communication with them — and to constrain the otherwise potentially infinite variety of API call types to be supported.
Message orchestrators call these interface methods in order to deliver requests and events to composable services — based upon the serviceName, serviceVersion, and method specified in the message API call used.
- public CSResponse get(String id) :: GET the service context representation matching the id.
- public CSResponse list(String queryString) :: GET a list of service context representations matching the queryString.
- public CSResponse create(Object body) :: POST a new service context representation instance using body. If the identifier in the body is null, create a new identifier for it.
- public CSResponse command(Object command) : POST (execute) a service command.
- public CSResponse replace(String id, Object modifiedBody) : PUT (replace) the instance of the service context representation matching the id with modifiedBody.
- public CSResponse patch(String id, Object JsonDiffs) : PATCH (apply) the changes described by jsonDiffs to the instance of the service context representation matching id.
- public CSResponse delete(String id) : DELETE the service context representation instance matching id.
- public CSResponse getInfo() : Use OPTION to return a representation of the class metadata and description of the composable service. Message orchestrators invoke this method when they first detect the presence of a composable service. If the composable service subscribes to a topic queue, the orchestrator will set that access up.
<back>