How can I prevent a denormalized contract overload for every combination of datasets required for service consumers?
Multiple denormalized contracts can be used to expose multiple groups of contract information, or customized sets of data pertaining to tailored datasets required for specific consumers. Overuse of denormalized contracts can bloat the service inventory with difficult to interpret, discover and govern contracts.
Instead of exposing a new contract for every group of data required, a compound denormalized contract is exposed, with configuration parameters to select which groups of data are required in the service output data.
Every logical group of data in the output can be selected by an enabler flag in the input. When a service consumer requires more datasets from the service, it enables more flags in the input message.
Concurrently executing various parts of core service logic may help improve performance.
Using this pattern increases the amount of exposed service metadata which is implicitly exposed in the service contract.
Overuse of this pattern can create large service contracts. Service composability can be jeopardized when the service contract becomes too big, as service autonomy is reduced. This pattern gives consumers a great deal of control on the actual service logic being executed and thereby reduces the amount of service autonomy. Up-front education of the service consumer designers is may be necessary to elaborate on the actual use of the enabler flags.
Because similar denormalized service contracts should be reusable for multiple consumers, it is difficult to express each contract in such a way that it easy for consumers to find out which contract to use. This causes difficulties during discovery and governance. Ie. when a consumer tries finding a contract, and finds that multiple similar contracts expose the same capability, it's hard to find the appropriate one.
For example, three service contracts can expose subset A, subset B, subset C and subset A+B of data to avoid too much load on back ends. This way it's possible to retrieve only subsets of the back end exposed data, not unnecessarily using back en resources for data that is not required for the consumer anyway. The consumer needs to know which service capability to use in the denormalized contract set to obtain the appropriate dataset pertaining to its needs. If many combinations of data for similar capabilities are required in a service inventory, many denormalized contracts are exposed, bloating the inventory with so many contracts, that discoverability is severely impacted. Furthermore, if a consumer over time requires a different dataset, the a new contract must be found and implemented which is - given the situation - an undesirable form of consumer-to-contract coupling.
Figure 1 - With a denormalized contract, for every combination of data, a separate contract is exposed to consumers. Ie. one for dataset A, one for dataset B and one for dataset AB. This increases the consumer to implementation coupling to the actual valid service contract combinations exposed.
To increase service discoverability as well as the reuse potential of a service, every individual capability has a selector/enabler flag in the service input contract. This flag can then indicate which capability to execute and which not. Instead of exposing one contract for each combination, this way, one contract is exposed for all possible combinations and the consumer can configure the executed set of capabilities by enabling flags in the input.
Figure 2 - For every capability exposed, the configurable contract exposes a selector flag to allow for consumers to selecting specific combinations of capabilities, ie. enableA, enableB, enableC.
A set of selector flags may reduce the amount of exposed contracts to as little as just one, which selects different underlying core service logic depending on the value of the parameters in the input message. Use of this pattern increases the amount of metadata exposed in the contract implicitly. Exposing too many options can cause response time issues, as multiple core service logic parts are executed. Application of the federated query pattern can mitigate for this if the service response becomes unacceptably slow.
Figure 3 - The execution of multiple combinations of normalized or segmented parts of core service logic are controlled by service façade logic, impacting which actual service logic is executed during a runtime call. This allows to abstract from the actual knowledge which combinations of denormalized contracts are available in the service inventory and resolves a service discovery challenge. This is especially relevant when different consumers have slightly different but similar requirements which vary only in the combination of service capabilities required.
From service point of view, as long as smart service contract versioning is applied, the service contract can be easily upgraded to allow for more capabilities. Many versions of the same service may come into existence so strong governance on versioning and service lifecycles must be applied. This can be done for example by making selector parameters optional (and default off).
From service consumer point of view, adding service capabilities is the same as changing service contracts. Smart versioning of contracts is required to prevent all service consumers from being affected by adding a capability, even if they don't require the new capability or set of data in the contract. This is caused by the inherent contract-to-implementation coupling in the service architecture caused by this pattern. Because all affected capabilities are exposed via one service, discoverability is increased when compared to the application of the denormalized contract, but in general, since the service is more course-grained, the service description in a service repository is harder to read when compared to implementing normalized service capabilities.
The inherent amount of reuse becomes larger as adding capabilities allows for more combinations which in turn allow for more reuse, the service can become quite popular and availability issues may arise. The redundant implementation pattern can be used to increase availability as well as to allow for distribution of load.
Because multiple capabilities are hidden behind a service façade, monitoring specific parts of the services, as well as predictability of load, performance etcetera of specific capabilities may decrease since the data is typically not available in a normalized fashion.
In order to implement the configurable contract, service façade logic is used to shield the selector logic from all core service logic which is being executed.
Figure 4 - Because configurable contract decomposes the interface into controllable parts, the service normalization is preserved.