Documentation for a newer release is available. View Latest
Esta página no está disponible actualmente en Español. Si lo necesita, póngase en contacto con el servicio de asistencia de Icon (correo electrónico)

How do I use the generated code?

This page explains how to get started with the code generated by the flo-lang project. It describes all the different interfaces that will be generated and reviews the central domain class used to interact with the domain itself.

The flo-lang language has three key levels to consider:

  • Solutions - this is effectively an overall project. A solution can contain many models.

  • Models - this represents a single self-contained domain. A model can contain many flows.

  • Flows - this is a single executable process flow.

From a generation perspective, generation is done at the model level.

This means that for each model an isolated set of code is generated that is not dependent on any other model. Each model will provide one central class for interacting with the domain, and that class can contain access to multiple different flows within it.

The Generated Interfaces

Action Adapters: Connecting to things outside the domain

For each external domain that is used within the model an action adapter interface is generated. This class will provide a method which is what the domain will call when it determines it needs to interact with the external system.

The implementation of the action adapter methods, therefore is expected to include the code to physically pass the request onto the respective external domain:

@Override
public CompletionStage<Void> execute(SampleExternalDomainAction action) {
    var externalDomainDto = convertToExternalDomain(action);
    return sampleExternalDomainConnector.send(action.getProcessingContext(), externalDomainDto)
            // during development it is sometimes useful to include debug information like this
            .thenAccept(report -> log.debug("Send completed with {}", report.getOutcome()));
}

Decision Adapters: Providing the business logic for the decision

If the domain uses any decisions, then a decision adapter will be generated. The respective decision adapter methods will be called when the flow decides it needs to execute a decision, therefore the implementation should contain the business logic that is required to execute that decision correctly.

Domain Function Adapters: Providing the business logic for domain functions

Domain functions are in reality very similar to external domains. The main difference is that whereas an external domain is expected to hand over control of processing to some external system, the domain function is expected to process that logic within it’s own boundaries.

If domain functions are used within the model, then a domain function adapter interface will be generated providing method definitions for each domain function to be used.

Aggregate Function Adapters: Providing additional data manipulation within a flow

An aggregate function provides the capability to hook into the arrival of an event within a flow and provide a utility to manipulate data. For each flow that uses aggregate functions, an interface will be generated. An implementation of these interfaces will be required.

Impacts of Aliasing

An alias is a type of business data concept that allows the call to one of the capabilities defined above to be called in different circumstances using different data sets. As a result, when alias’s are used then the generation may produce more than method on each interface, but with different parameter inputs to reflect the different aliases implementations.

Use of external models

It is possible to define various supporting parts of a flow in a different model. For example, you may choose to define your external domains in a separate model that can then be reused from multiple different models rather than defining the same external domains repeatedly within different functional models. From a generation perspective, this is no different to the domains having been defined locally within the functional model itself.

Building the domain

When a model is generated, it will generate a class that represents the domain as a whole. This class will be called <ModelName>v<VersionNumber>Domain (note that if there is no version number then just the model name is used). This class provides all the functionality to interact with the domain.

Building the domain class

A Builder is provided with the domain class, it allows for the provision to the domain of everything it needs to be able to work:

  • For each external domain used in the model, an implementation of the appropriate action adapter should be provided using the with<ActionAdapterName>ActionAdapter methods.

  • If decisions are required, an implementation of the model’s decision adapter interface should be provided using the withDecisionAdapter method.

  • If domain functions are required, an implementation of the model’s domain function adapter should be providing using the withDomainFunctionAdapter method.

When building the domain, the minimum that must be specified is:

  • The actor system for it to run on

  • Implementations of all the generated interfaces (external domains, domain functions & decisions)

The following shoes how a simple domain may be built.

 new XYZDomain.Builder(actorSystem)
                .withDomainFunctionAdapter(new SampleXYZDomainFunctionAdapter())
                .withDecisionAdapter(new SampleXYZDecisionAdapter())
                .withABCFlowAggregateFunctionAdapter(new SampleABCFlowAggregateFunctionAdapter())
                .withAnExternalDomainActionAdapter(new SampleExternalDomainActionAdapter())
                .build();

When the build method is invoked, then all the flows within the model will be started.

Optional domain parameters

When configuring the domain, in addition to the mandatory parameters defined above it is possible to define a number of optional attributes which, if not provided, are defaulted.

  • EventBus: the event bus implementation to be used when the domain raises system events.

  • ConfigAdapter

  • SchedulerAdapter

  • RetrySupport

  • EventProcessorSettings (per flow)

  • MetaDataExtractor (per flow)

Using the domain

Handling responses from an external domain

The domain class provides methods to be able to provide responses back into the domain. These are split out on a per external domain basis. To invoke these we call:

 XYZDomain.externalDomainPort().handle(new ExternalDomainResponseInput());

The result of the handle method call is a CompletableFuture which completes once the flow has applied the input to the current state. The future carries with it a Done object, which provides a detailed view of what the end result of applying the input was:

@AllArgsConstructor
@Getter
public class Done implements CborSerializable {
    private final String aggregateId;
    private final String aggregateStatus;
    private final String commandName;
    private final Result result;

    public enum Result {
        EXECUTED, DUPLICATE, UNEXPECTED, ERROR
    }
}

Out of the listed fields, the result is in most cases the important one to look at. Result it can have the following values, each representing a different outcome:

  • EXECUTED means that the input was successfully applied to the current state but offers no guarantees about the resulting operations (actions, notifications, domain functions)

  • DUPLICATE means that the input was deemed a business duplicate of a previously handled input and was thus not applied to the current state

  • UNEXPECTED means that the input was deemed inappropriate for the current state of the flow and was thus not applied to the state

  • ERROR means that an issue was encountered while executing one of the operations that were triggered as a result of applying the input (actions, notifications, domain functions)

Generally speaking, unless you have specific requirements (e.g. raising a system event on certain duplicate responses), you can ignore the outcome of applying the input and consider the operation a success as long as the future itself doesn’t fail.

Handling of duplicate responses from an external domain

By default, the external domain port that the domain provides comes bundled with configurable retry support.In order to ensure that retries are not interpreted as independent responses from the external domain, the port will assign random UUIDs as inputId and physicalMessageId if those values are not set on the input object.

  • inputId represents a unique business identifier (from the perspective of the flow) of an external domain’s response and is the identifier based on which idempotent processing will be performed

  • physicalMessageId represents a unique transport identifier of an external domain’s response, and it is useful for cases where the same physical message can be consumed multiple times (e.g. coming from message brokers), and the system needs to distinguish between a duplicate consumption (same message consumed twice) and a duplicate message (two messages with the same resulting inputId)

When using the IPF connector framework, all supported broker-based receive connectors will contain a suitable PhysicalMessageId within their ReceivingContext object, which can then be used to populate the input.

If the flow receives two inputs with the same inputId, the second input will always skip execution and either result in a Done.Result.EXECUTED or a Done.Result.DUPLICATE outcome, depending on the physicalMessageId.If the physicalMessageId is also the same on both inputs, the second input is treated as a duplicate consumption and Done.Result.EXECUTED is returned.Otherwise, the input is treated as a duplicate message and Done.Result.DUPLICATE is returned.

While the default approach works for most scenarios, there are some corner cases where it may not be enough to ensure correct processing in your flows - e.g. when your flow handles the same input type from multiple non-exclusive states - thus requiring you to specify at least an inputId in order to avoid duplicate consumptions incorrectly triggering a flow transition:

 XYZDomain.externalDomainPort().handle(new ExternalDomainResponseInput
    .Builder(id, ExternalDomainResponseCodes.OK)
    .withInputId(someInputIdFromResponseMessage)
    .build());

Calling another flow from an action

When communicating between different flows that are part of the same logical unit of work, it is important to pass along the current flow’s EventId to the other flow. An EventId carries within itself its owner flow’s Lamport timestamp that the other flow will continue incrementing. Thus, by passing along an EventId from one flow to another, we enable domain events - belonging to different flows within a single logical unit of work - to be ordered in a causal, happened-before order.

Every generated Action provides access to the current EventId via its getCurrentEventId method, whereas every Input.Builder provides a withCausedByEventId that can be used to pass along the EventId of the collaborating flow:

@Override
public CompletionStage<Void> execute(SampleExternalDomainAction action) {
    var externalDomainDto = convertToExternalDomain(action);
    return AnotherDomain.appropriateDomainPort().handle(new AppropriateDomainResponseInput
                .Builder(id, AppropriateDomainResponseCodes.OK)
                .withCausedByEventId(action.getCurrentEventId())
                .build());
}

Being able to determine the causal order of events in a distributed computer system is crucial when building read-side projections of a unit of work based on the domain events of its constituent flows. Without EventIds and their Lamport timestamps, projections would be forced to use system clock timestamps in their stead and as different nodes or processes will typically not be perfectly synchronized, clock skew would apply and incorrect event order would be arrived at as a result.

Informing the domain that a payment has timed out

When a payment times out, it is necessary to tell the domain that this has occured. To do this, a helper class is available on the domain.

 XYZDomain.timeout().execute(aggregateId, actionName);

Ad-hoc functions within the domain

The domain class also provides the ability to perform ad-hoc functions within the flow these are:

  • Aborting a payment

  • Accessing the payments status

  • Accessing the payments aggregate

These can all be done directly by calling the appropriate method on the domain class itself:

 XYZDomain.abort(aggregateId, reason);
 XYZDomain.getStatus(aggregateId);
 XYZDomain.getAggregate(aggregateId);

Identifying the available flows and versions

To be able to clearly understand what flows are available within a generated domain, we can interogate the domain class. For each flow we have a static access available as per below:

 XYZDomain.Flows.AFlow.name();
 XYZDomain.Flows.AFlow.getAggregate();
 XYZDomain.Flows.AFlow.getLatestVersion();