Integration

Scope of problem

Unlike the simple examples, the Condense framework is aimed at more complex domain models that change over time and may contain multiple code-versions of business logic. The event-based nature also means that from a given perspective, eg a user interface, a portion of the domain is irrelevant or out of scope. For example;

Additional processing that continues after a relevant result is achieved. After a successful payment in a web application, the various pieces of the shipment process are orchestrated for back office systems.

Side effects that are not relevant to the current perspective. During a loan approval, various sub-processes publish data relevant to auditing or government oversight.

Processes that internally require interaction from a different perspective. During a loan approval on web, certain conditions require additional input from the perspective of a staff user.

To handle these situations, the Condense framework decouples the interaction with the domain from the execution of domain logic. This is done using the integration features, which turn a simple interface definition into an implementation that publishes new data into the domain, and subscribes to the appropriate results.

Integration Interfaces

To interact with the domain, you must declare an interface in code. The runtime will provide you with an appropriate implementation of this interface. Condense Cloud will provide a web service endpoint, and Condense Light will provide an implementation by calling GetIntegration<T>().

The implementation of the interface will follow the semantics described here.

Simply

A method of signature TResult YourMethodName(TInput a, TInput b...) declares the following:

By calling this method, you are providing new entities a and b. These will be stored. They will trigger off various work in the domain. You expect that at some stage, in the context of either of those entities, that a TResult will be created. The implementation will block until this result appears, and will return it, or throw a TimeoutException.

A void return type will return immediately. You may also specify causality for the new entities using a parameter decorated with the LambdaCausality attribute, expecting the key of the causing entity. If the context of the input entities is not appropriate, it can be provided by key on a string parameter using the LambdaContext attribute, or this attribute can be placed on the method and an appropriate context will be found based on the provided causality.

Method Signature Semantics

{void | TResult} {optional [LambdaContext(typeof(TContext)]}
    YourMethodName(
        {optional [LambdaContext(typeof(TContext))] string contextKey,} 
        {optional [LambdaCausality(typeof(T))] string causalityKey, ... }
        {optional TEntity newEntity1, ... }
    );

An entity which is provided as an argument is considered new or revised, and will be stored. If any causality parameters are provided, then they will become part of the entity’s lineage. It will then trigger any executions as appropriate.

A method that is declared to return an entity type TResult will return the first result in context which is coercible to that type. A void type will return immediately.

A specific context may be specified directly by providing an entity key and type T using a string parameter decorated with [LambdaContext(typeof(T))]

Or a context may be inferred from the specified causality by decorating the method with [LambdaContext(typeof(T))]. In this case the context will be set to the most recent entity which is coercible to type T which exists in the lineage of each specified causality.

If a context is not specified, the method will return the first entity coercible to type TResult which is caused by any of the inputs (it’s lineage contains any input).

Failures

The method call will fail and any actions will roll back in the following circumstances:

  • The method signature is invalid (eg: context is specified on the method and on a parameter) or a provided parameter is null or invalid.

  • Any failure to communicate with the storage subsystem, work queue, or subscription queue.

  • A context or causality specified by key cannot be found.

  • A context specified by type cannot be found in the lineage of every entity specified as a cause.

  • A lambda flagged as in-process chaining, triggered by one of the entities to be stored, and for which a valid execution plan could be determined, was executed, but threw an exception.

Asynchronity & Isolation

The integration method does not specify how the result is achieved. This is the responsibility of the domain, and may be arbitrarily complex, or depend on other systems. Generally speaking, an integration interface will match the caller’s needs; eg: LendingDecision ApplyForLoan(LoanApplication app).

The integration will validate parameters, store provided entities, and then enqueue any execution requests. When this transaction is committed, the domain is isolated from the integration - and execution will continue according to specification.

The integration waits on any results using the subscription service.

If the timeout waiting for a result is exceeded, the result may still be available in the future.



On timeout, an immediate call to an integration method like TResult WaitLongerForResult( {context parameters} ) would allow the callee to continue waiting. However the result might be missed in the small window between timeout and recall. As increasing timeouts does not cover all similar use-cases (particularly in a web environment) it will be addressed with a combination of look-behind (or catchup) subscriptions, and the optional use of future-results. async language features will also be supported.

Lambdas that are flagged for in-process chaining will be executed immediately as part of the initial storage/enqueue transaction. If any of these fail, the entire transaction will be rolled back, and will not be retried. This makes them seem useful for validation purposes (and indeed you could create quite complex contextual validation using this technique); however, it is important to realize that “in-process chaining” is intended as an optimisation, not a guarantee. A more formal method of specifying this will be introduced.

Result

This feature decouples your application code from your domain logic, and also isolates the domain from common bugs related to transactions, locking, and early termination.

As these integration points do not contain any business logic, multiple interfaces may safely interact with the same set of entities, and you can be assured that the business logic in your lambda methods is executed according to the rules of your domain.

Your application is insulated from this complexity, although of course when designing the interaction you will need to consider that some of these integration methods will return immediately, and others may unavoidably block for some time if they are waiting on input from another integration. Regardless, the rules around lambda executions and integrations should be consistent.

Subscriptions

The integration framework also exposes some of the subscription features to your application.

object Subscribe<TContext, TResult>(string contextId, Action<TResult> action);
void Unsubscribe(object subscription)

This allows you to subscribe to all domain types which inherit from TResult in the context of an given entity of type TContext with Uid contextId. An arbitrary object reference is provided to act as a token for your subscription for unsubscription.