Cordaptor extensions: the ‘why’ and the ‘how’
Cordaptor is an open source project offering a way to instantly add Corda to any technology stack and greatly reduce the learning curve of Corda integration. It also provides powerful features helping to achieve greater resiliency, availability, and security of the overall application architecture.
Cordaptor is based on a proprietary code developed and battle-tested by Bond180 as part of the work on its own digital assets issuance and administration platform IAN in early 2020. I was actively involved in that project. When we decided to walk an extra mile to make our prior work available to the wider Corda community as an open source project, as project’s principal engineer, I had to decide whether to make any architectural changes to the original code.
It was clear from the beginning that Cordaptor must cater for two distinct deployment models. Firstly, a zero-configuration light-weight instantiation as a Corda service within a node to expose an API for a CorDapp, which would be an excellent aid for automated integration tests, as well as informed exploratory testing. Secondly, it must also run in a standalone JVM connecting to the node via Corda RPC and offering the benefits of a caching gateway enhancing the availability of the node to the API clients. Corda APIs differ somewhat between what is available in-process to a service vs RPC operations, and I wanted to minimise the impact of the discrepancies on the rest of the codebase, so some kind of SPI was necessary.
Another important consideration I was mindful of is the fact that Corda API is bound to evolve. Even though Corda for many years only added features, R3 have publicly stated their desire to break backwards compatibility in the upcoming Corda 5 release. That aside, some features appearing in recent versions of Corda (e.g. retrieving flow results using clientId since 4.6) may be possible to support for earlier versions (e.g. via flow snapshots endpoint) via a compatibility layer.
Finally, it was inevitable that most real-world applications would have their own specific deployment needs (especially around API endpoint security), as well as idiosyncrasies of CorDapps that make automatically generated REST API unwieldy for the client. This necessitated a requirement to allow the core functionality to be extended at key points without forcing the adopting teams to do an outright fork, which would not be as beneficial for Cordaptor open source community.
In the light of the above I decided to go with a microkernel architecture for Cordaptor, in which extensions are the first-class citizens. Cordaptor at its core contains a fully-featured application microkernel, whose sole job is to locate, dynamically load, and initialize all extension modules it can find in the classpath. With that, any Cordaptor distribution is effectively a bundle of standard modules, which are initialized by the microkernel, and, by working collaboratively, implement the overall functionality.
Depending on the distribution, different modules may provide similar functions, ensuring consistent observable behaviour despite the discrepancies. For example, cordaptor-rest-endpoint module embeds the Undertow web server and is responsible for generating REST API for all CorDapps. However, the actual CorDapp discovery and the use of appropriate Corda API (either internal Corda service API or remote Corda RPC) is provided by one of two modules: cordaptor-corda-service or cordaptor-corda-rpc-client respectively. For the REST API endpoints implementation in cordaptor-rest-endpoint, there is no difference, because advertised (public) inter-module APIs are the same, and consequently there is no need to code and maintain multiple implementations of the endpoints operations.
This architecture has far reaching consequences beyond merely ensuring the API compatibility and optimizing the size of the core. There are many valid reasons for providing further extensions for Cordaptor. The following list gives some examples (adapted from the documentation):
- Developers may choose to add custom REST API endpoints to extend and/or tailor the API provided out of the box (see below).
- Developers may provide bespoke implementations for authentication and authorization logic by using PAC4J API. For example, integrating with enterprise-specific single sign-on infrastructure for service accounts.
- Developers may provide bespoke implementations for secrets management depending on their requirements by extending basic implementation provided as part of the microkernel
- Future versions of Corda may introduce new features or breaking API changes, which would require different logic to be implemented to support the functionality of Cordaptor API operations. Such implementations may need to be compiled against different versions of Corda libraries.
- Developers may chose to use different transport protocols for Cordaptor API, e.g. GraphQL or gRPC.
- Developers may need to override default JSON serialization behaviour for some types. This is particularly pertinent to situation when the CorDapp API needs to be published to other teams.
In my view, the first scenario above is especially important for getting the most out of Cordaptor. The ideal deployment architecture is that a user interface or other enterprise systems could be integrated directly with Corda by using the API generated by Cordaptor. However, in some not-too-rare cases the API would require further handling before it can be offered to the clients. I can give two examples:
- A CorDapp flow may require a parameter which is contextual to the deployment and is not something that an API client can be reasonably expected to provide. A very typical example would be an X.500 name of a counterparty, which is specific to now a particular Corda network is managed. At the same time the client code is likely to know an internal reference only. There is a need to translate between the two, e.g. from JPM to “O=J.P. Morgan Bank (Ireland) plc,L=London,C=GB”.
- A CorDapp may require a deliberate validation of some API calls, especially if the API is published to another team or even a client. Cordaptor by default only performs structural validation (omitted mandatory values, compatible JSON types, enum values, etc), but not any business-domain validation (non-negative values, allowed country/currency codes, etc). Whilst many of these validations are possible to introduce at the level of an API gateway in front of Cordaptor, some more sophisticated logic is better to be implemented closer to the API, especially if it requires to use Corda API in itself.
What I wanted to avoid at all reasonable costs is the need to create a ‘facade’ microservice in front of Cordaptor just to make API adaptations like the above. Whichever technology you use for the facade, it will introduce another hop over the network, another occurrence of the serialization overhead, and another set of infrastructure components to configure and monitor. Therefore, Cordaptor itself could be considered an application server, providing internal APIs that application developers could use to create application-specific extensions and customizations to the CorDapp API without much incremental complexity in their architectures.
For extensions developers Cordaptor microkernel and its core modules provide a rich set of internal (public) APIs, which could be used to develop meaningful extensions without unnecessary coupling to a specific version of the core, compared to what a fork would introduce. The below is a brief summary of the capabilities available to Cordaptor extensions as of version 0.1 either through the microkernel or via the core modules:
- Dependency injection powered by Koin framework
- Consistent API for module configurations powered by lightbend/config
- Extensible secrets management framework
- Dynamic module discovery and initialization from the classpath
- Lifecycle events notifications and management
- Consistent logging API for the modules powered by SLF4J and using log4j 2.x.
- Deployment-agonistic Corda API exposing key functionality of the node regardless whether it’s accessed directly or through the RPC
- Comprehensive JSON serialization/deserialization framework using Corda introspection logic under the hood
- Last, but not least, a lightweight framework for creating REST API endpoints which are automatically added to the web server and to OpenAPI specification.
This list is likely to evolve further as Cordaptor itself gets additional features. For example, we would like to introduce support for WebSockets in one of the upcoming versions, and internal APIs are likely to be made available to extension developers as well.
If you are interested in developing extensions for Cordaptor because some of of the above reasons apply in your case, I recommend starting with a comprehensive introduction to extensions as part of Cordaptor documentation on github. If you have any specific questions, please feel free to reach us on #cordaptor channel on Cordaledger Slack, or by email management@bond180.com