Unit 11: Implement Commands and Publish Events
In this course you will learn how to implement Factory and Instance Commands of a root entity using your local IDE.
Outlineโ
For our first implementation we will start with coding the commands as they have a direct influence on the root entities, the core of our domain service project. As explained in Unit 3: Design Commands, a Factory Command is used to create a new instance and the Instance Command is used to manipulate existing instances. Furthermore, commands can have an input entity, but no output, and they can publish business events and throw business errors. The code generation prepares the boilerplate code and implementation stubs for us to dive right into implementing the business logic.
In general, the implementation of commands follows this pattern:
- Based on the command type, create an instance or manipulate it. If the conditions are not met to do so, throw a business error.
- Persist the changes of the instance in the database.
- Publish an event to communicate the changes with other services.
Prerequisitesโ
- You have successfully completed Unit 10: Set up Project for Implementation.
- You have successfully completed Unit 3: Design Commands.
If you rather want to use this course as a starting point of your training, you can use a different asset where all courses for designing are already completed.
In this case - depending on the chosen implementation language - use either asset "Order_Java_Code_0.1" or "Order_TypeScript_Code_0.1" to create a new project and to continue your training.
You can look up how to create a new project from the Order assets in the Preparation section in the Course Introduction.
Exerciseโ
Estimated time: 30 minutes
Exercise goal: After completing this course you are able to apply all parts of the above mentioned implementation pattern to Factory and Instance Commands.
Supported languages: Java and TypeScript
In this exercise we will implement two commands. First the Factory command which creates a new order instance and then the Instance Command which cancels an existing order instance. Both commands will persist the altered instance and publish an event to inform about the changes.
Step 1: Implement Factory Commandโ
Our command to create a new order will have the following parts:
- Determine the type of order to be created
- Create the order instance with the provided data
- Persist the new instance in the database
- Publish the OrderStatusChanged event
- Java
- TypeScript
-
Open the file
src/main/java/<package-name>/domain/ord/command/OrderCommand.java.
You will see that the file contains an auto-generated stub with a classOrderCommandand an empty methodcreateOrder. The implementation of the command needs to be added inside the method block.<package-name>: The package name of the Java project (e.g.com.knowis.orderjfinal)@Service("ord_OrderCommand")
public class OrderCommand extends OrderCommandBase {
private static final Logger log = LoggerFactory.getLogger(OrderCommand.class);
public OrderCommand(
DomainEntityBuilder entityBuilder,
DomainEventBuilder eventBuilder,
EventProducerService eventProducer,
Repository repo
) {
super(entityBuilder, eventBuilder, eventProducer, repo);
}
...
@Override
public Order createOrder(
CreateOrderInput createOrderInput
) {
log.info("OrderCommand.createOrder()");
// TODO: Add your command implementation logic
return null;
}
...
} -
First, we will create a new instance of an order with the provided input. Depending on the order type, either a customer order or an internal order is created.
// Step 1: Create the new instance
// Initialize a new variable that will later be assigned with the new order
Order newInstance;
// Distinguish the type of order that will be created by the input
if (createOrderInput.getOrderType().equals(OrderType.CUSTOMER)) {
// Create a new CustomerOrder and set the child class specific properties
newInstance = this.entityBuilder.getOrd().getCustomerOrder()
.setCustomerReferenceId(createOrderInput.getReferenceId())
.build();
}
else if (createOrderInput.getOrderType().equals(OrderType.INTERNAL)) {
// Create a new InternalOrder and set the child class specific properties
newInstance = this.entityBuilder.getOrd().getInternalOrder()
.setInternalReferenceId(createOrderInput.getReferenceId())
.build();
}
else {
// Return early if no valid order type is provided
return null;
}
// Set the properties that are in the parent class Order
newInstance.setStatus(Status.OPEN); // set status to OPEN
newInstance.setCreatedOn(OffsetDateTime.now()); // set the creation timestamp to the current time
newInstance.setOrderItems(createOrderInput.getOrderItems()); // set the order items from the input
// For property totalPrice (of property type Currency) create a helper variable
BigDecimal totalAmount = newInstance.getOrderItems().stream()
.map(orderItem -> orderItem.getPrice().getAmount())
.reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
Currency currencyEntity = (newInstance.getOrderItems().size() > 0)
? newInstance.getOrderItems().get(0).getPrice().getCurrency()
: Currency.getInstance("EUR");
Money totalPrice = new Money();
totalPrice.setAmount(totalAmount); // set the initial amount
totalPrice.setCurrency(currencyEntity); // set the currency
newInstance.setTotalPrice(totalPrice); // add the initialized totalPrice to newInstancecreateOrderInput: provides type-safe access to the properties modeled in the input entity of the commandthis.entityBuilder.getOrd(): provides the constructors to create each kind of entity in domain namespaceord -
Now we have to persist the changes of the new instance to the database.
// Step 2: Persist the new instance
Order persistedInstance = this.repo.getOrd().getOrder().save(newInstance);this.repo: provides access to load and save instances of the root entities in the database. -
Once the order has been created and persisted successfully, we will publish the OrderStatusChanged event.
// Step 3: Publish the OrderStatusChangedEvent
// Create the event payload
TrainingOrderStatusChanged1x1Schema eventPayload = new TrainingOrderStatusChanged1x1Schema();
eventPayload.setOrderId(persistedInstance.getId());
eventPayload.setOrderNewStatus(TrainingOrderStatusChanged1x1Schema.OrderNewStatus.OPEN);
// Extract the referenceId dependent on the instance type
String referenceId = (persistedInstance instanceof CustomerOrder)
? ((CustomerOrder) persistedInstance).getCustomerReferenceId()
: ((InternalOrder) persistedInstance).getInternalReferenceId();
eventPayload.setReferenceId(referenceId);
// Create and publish event
OrderStatusChanged event = this.eventBuilder.getOrd().getOrderStatusChanged()
.setPayload(eventPayload)
.build();
this.eventProducer.publish(event);
log.info(TextFormatter.format(
"CreateOrder published OrderStatusChanged event, order ID = {}", persistedInstance.getId()
));this.eventBuilder: provides functionality to create events which have been added to the command in the designTrainingOrderStatusChanged1x1Schema: represents the event schema modeled in the schema registry of the Solution Designerthis.eventProducer.publish(event): publishes the created event -
Return the order instance, as factory commands always have to return the persisted instance.
// Step 4: Return the persisted instance
return persistedInstance; -
The root entity
Orderhas a childCustomerOrder. Each command modeled for the parent root entity has to be implemented in its children as well.Therefore, open the file
/src/main/java/<package-name>/domain/ord/command/CustomerOrderCommand.java. You will find (like for root entity Order) the methodcreateOrder, where the auto-generated stub looks like the following:@Override
public CustomerOrder createOrder(CreateOrderInput createOrderInput) {
log.info("CustomerOrderCommand.createOrder()");
// TODO: Add your command implementation logic
return null;
}Since the functionality of command
CreateOrderdoes not differ in the children of root entityOrder, you can use the command of the parent. Replace null with(CustomerOrder) super.createOrder(createOrderInput), so that the methodcreateOrderlooks like the following:@Override
public CustomerOrder createOrder(CreateOrderInput createOrderInput) {
log.info("CustomerOrderCommand.createOrder()");
// Execute the code of the base class
return (CustomerOrder) super.createOrder(createOrderInput);
} -
Open the file
/src/main/java/<package-name>/domain/ord/command/InternalOrderCommand.javaand repeat step 6 for root entityInternalOrder. Replace(CustomerOrder)in the return statement with(InternalOrder).
-
Open the file
/src-impl/domain/ord/aggregates/Order/factory/CreateOrder.ts.
You will see that the file contains an auto-generated stub with a more or less emptyexecute()method block that you need to fill with code to implement the actual functionality. The only content of that block is commented out sample code for some often-used functionality aimed at serving as a template for what you might want to do.export default class extends commands.ord_CreateOrder {
public async execute(): Promise<void> {
const log = this.util.log;
log.debug('ord_CreateOrder.execute()');
// Exemplary implementation:
// Assign the created entity to this.instance. This will also ensure type safety
// And the factoryCommand will return the correct instanceId automatically
// this.instance = this.factory.entity.ord.Order();
// Persist the instance (if changes were made)
// await this.instance.persist();
}
public async available(): Promise<boolean> {
return true;
}
} -
First, we will create a new instance of an order with the provided input. Depending on the order type, either a customer order or an internal order is created.
// Step 1: Create the new instance
// Initialize a new variable that will later be assigned with the new order
if (this.input.orderType === 'CUSTOMER') {
this.instance = this.factory.entity.ord.CustomerOrder();
this.instance.customerReferenceId = this.input.referenceId;
} else if (this.input.orderType === 'INTERNAL') {
this.instance = this.factory.entity.ord.InternalOrder();
this.instance.internalReferenceId = this.input.referenceId;
} else {
return;
}
this.instance.createdOn = new Date(Date.now());
this.instance.status = 'OPEN';
this.instance.orderItems = this.input.orderItems;
// For property totalPrice (of property type CurrencyValue) create helper variables
const currency = this.input.orderItems ? this.input.orderItems[0].price.currency : "EUR";
const totalAmount = this.input.orderItems.reduce((sum, item) => {
return sum.plus(item.price.amount);
}, new BigNumber(0));
this.instance.totalPrice = {
currency: currency,
amount: totalAmount
};this.input: provides type-safe access to the properties modeled in the input entitythis.factory.entity.ord: provides the constructors to create each kind of entity in domain namespaceord -
Now we have to persist the changes of the new instance to the database.
// Step 2: Persist the new instance
await this.instance.persist();this.instance.persist(): persists the instance in the database. As this operation is executed asynchronously you need to add anawaitto the call. -
Once the order has been created and persisted successfully, we will publish the OrderStatusChanged event.
// Step 3: Publish the OrderStatusChangedEvent
// Create the event payload
const orderStatusChangedEvent = this.factory.event.ord.OrderStatusChanged();
const payload: TrainingOrderStatusChangedV1_1 = {
orderId: this.instance._id,
orderNewStatus: this.instance.status,
referenceId: this.input.referenceId
};
// Create and publish event
orderStatusChangedEvent.payload = payload;
await orderStatusChangedEvent.publish();
log.info('CreateOrder published OrderStatusChanged event, order ID = ' + this.instance._id);this.factory.event: provides functionality to create events which have been added to the command in the designTrainingOrderStatusChangedV1_1: represents the event schema modeled in the schema registry of the Solution DesignerorderStatusChangedEvent.publish(): publishes the created event. This is also an asynchronous operation
Step 2: Implement Instance Commandโ
Our command to cancel an existing order will provide the following functionality:
- Check the current status of the order, if not "OPEN" or "IN_PROGRESS" a business error will be thrown
- Change the status of the order to "CANCELLED"
- Persist the data in the database
- Publish the OrderStatusChanged event
- Java
- TypeScript
-
Open the file
/src/main/java/<package-name>/domain/ord/command/OrderCommand.java.
You will find the empty methodcancelOrderwith an auto-generated signature that contains the instance, the modelled input and the defined Business Error in it.<package-name>: The package name of the Java project (e.g.com.knowis.orderjfinal)@Override
public void cancelOrder(Order instance)
throws OrderCannotBeCancelled {
log.info("OrderCommand.cancelOrder()");
// TODO: Add your command implementation logic
}We will start the implementation of the
CancelOrdercommand. Therefore, we switch into thecancelOrdermethod and extend it with the code in the following steps. -
First we need to check whether the Order has a valid status to cancel the order. If not, a business error has to be thrown.
// Step 1: Check the status of the order, if not OPEN or IN_PROGRESS throw the business error
List<Status> allowedStatusList = List.of(Status.OPEN, Status.IN_PROGRESS);
if (!allowedStatusList.contains(instance.getStatus())) {
throw new OrderCannotBeCancelled();
}instance: provides access to the root entity instance for which the command is executedthrow new OrderCannotBeCancelled(): throws the business error which has been added to the command in the design -
Now we will update the status of the instance.
// Step 2: Change the status of the order to CANCELLED
// Store the old status for the later published event
Status oldStatus = instance.getStatus();
instance.setStatus(Status.CANCELLED); -
Then we have to persist the instance.
// Step 3: Persist the updated instance
Order persistedInstance = this.repo.getOrd().getOrder().save(instance); -
Once the order has been successfully cancelled and persisted, we will publish the OrderStatusChanged event.
// Step 4: Publish the OrderStatusChangedEvent
// Create the event payload
TrainingOrderStatusChanged1x1Schema eventPayload = new TrainingOrderStatusChanged1x1Schema();
eventPayload.setOrderId(persistedInstance.getId());
eventPayload.setOrderNewStatus(TrainingOrderStatusChanged1x1Schema.OrderNewStatus.CANCELLED);
eventPayload.setOrderPreviousStatus(TrainingOrderStatusChanged1x1Schema.OrderPreviousStatus.valueOf(oldStatus.name()));
// Extract the referenceId dependent on the instance type
String referenceId = (persistedInstance instanceof CustomerOrder)
? ((CustomerOrder) persistedInstance).getCustomerReferenceId()
: ((InternalOrder) persistedInstance).getInternalReferenceId();
eventPayload.setReferenceId(referenceId);
// Create and publish event
OrderStatusChanged event = this.eventBuilder.getOrd().getOrderStatusChanged()
.setPayload(eventPayload)
.build();
this.eventProducer.publish(event);
log.info(TextFormatter.format(
"CancelOrder published OrderStatusChanged event, order ID = {}", persistedInstance.getId()
)); -
The root entity
Orderhas a childCustomerOrder. Each command modeled for the parent root entity has to be implemented in its children as well.Therefore, open the file
/src/main/java/<package-name>/domain/ord/command/CustomerOrderCommand.java. You will find (like for root entity Order) the methodcancelOrder, where the auto-generated stub looks like the following:@Override
public void cancelOrder(CustomerOrder instance)
throws OrderCannotBeCancelled {
log.info("CustomerOrderCommand.cancelOrder()");
// TODO: Add your command implementation logic
}Since the functionality of command
CancelOrderdoes not differ in the children of root entityOrder, you can use the command of the parent. Add the linesuper.cancelOrder(instance), so that the methodcancelOrderlooks like the following:@Override
public void cancelOrder(CustomerOrder instance)
throws OrderCannotBeCancelled {
log.info("CustomerOrderCommand.cancelOrder()");
// Execute the code of the base class
super.cancelOrder(instance);
} -
Open the file
/src/main/java/<package-name>/domain/ord/command/InternalOrderCommand.javaand repeat step 6 for root entityInternalOrder.
-
Open the file
/src-impl/domain/order/aggregates/Order/instance/CancelOrder.ts.
You will see that similar to the factory command there is a stub auto-generated in which you can start your implementation.export default class extends commands.ord_CancelOrder {
public async execute(): Promise<void> {
const log = this.util.log;
log.debug('ord_CancelOrder.execute()');
// change state of instance
// Persist the instance (if changes were made)
// await this.instance.persist();
}
public async available(): Promise<boolean> {
return true;
}
}In the following steps, we will add the code of the
CancelOrdercommand inside theexecutemethod. -
First we need to check whether the Order has a valid status to cancel the order. If not, a business error has to be thrown.
// Step 1: Check the status of the order, if not OPEN or IN_PROGRESS throw the business error
if (this.instance.status !== 'OPEN' && this.instance.status !== 'IN_PROGRESS') {
throw ord_OrderCannotBeCancelled;
}this.instance: provides access to the root entity instance for which the command is executedthrow ord_OrderCannotBeCancelled: throws the business error which has been added to the command in the design -
Now we will update the status of the instance.
// Step 2: Change the status of the order to CANCELLED
// Store the previous status for the later published event
const previousStatus = this.instance.status;
this.instance.status = 'CANCELLED'; -
Then we have to persist the instance.
// Step 3: Persist the updated instance
await this.instance.persist(); -
Once the order has been successfully cancelled and persisted, we will publish the OrderStatusChanged event.
// Step 4: Publish the OrderStatusChangedEvent
// Create the event payload
const orderStatusChangedEvent = this.factory.event.ord.OrderStatusChanged();
const payload: TrainingOrderStatusChangedV1_1 = {
orderId: this.instance._id,
orderNewStatus: this.instance.status,
orderPreviousStatus: previousStatus,
referenceId: this.isInstanceOf.entity.ord.CustomerOrder(this.instance) ? this.instance.customerReferenceId : this.instance.internalReferenceId
};
// Create and publish event
orderStatusChangedEvent.payload = payload;
await orderStatusChangedEvent.publish();
log.info('CancelOrder published OrderStatusChanged event, order ID = ' + this.instance._id);
Step 3: Import missing Classesโ
After implementing the command, your IDE may highlight errors in the code. This is likely due to missing or incorrectly imported classes.
In general, the classes used in the implementation have four different fundamentals:
- Generated classes based on your implementation design (in our case what we modeled in courses 1 - 9)
=> split up into API, Domain and Integration Namespaces - Generated classes reflecting the capabilities of the IBM DevOps Solution Workbench (These classes are available in each implementation project)
- Built-in classes based on the framework of your selected implementation language
- Classes from other implementation files
Therefore, we will demonstrate how imports are applied for Java and TypeScript in general terms based on the above fundamentals and specifically based on implementation of the two commands above.
Many IDEs offer capabilities to import classes with a few clicks. If you are aware from which package the classes have to be imported, it makes more sense to import them during adding code than afterwards. In the following courses, we will follow this approach.
- Java
- TypeScript
Import Classes - General Approach
-
Importing generated classes based on your implementation design follows this pattern:
import <package-name>.sdk.*
Depending on which namespace the class is imported from, the import statements have the following pattern:Namespace-Type Import Pattern API import <package-name>.sdk.api.*Domain import <package-name>.sdk.domain.*Integration import <package-name>.sdk.integration.* -
Importing generated classes reflecting the capabilities of the IBM DevOps Solution Workbench follows this pattern:
import k5.sdk.springboot.* -
Importing built-in classes in Java does not follow a specific pattern, but here are some examples:
import java.math.BigDecimal;
import java.util.List;
import org.slf4j.Logger; -
Importing classes from other implementation files follows a pattern similar to the first fundamental (except
*.sdk.*), and goes like this:import <package-name>.*
Import Classes for the implemented Commands
-
Since we implemented the commands for root entity Order and its two children CustomerOrder and InternalOrder, all three corresponding files have to be edited. The following collapsibles show the complete list of imports in all the files.
Compare the list below with your current imports and add the missing classes for each file!
List of Imports - File
OrderCommand.java// Fundamental 1
import com.knowis.orderjfinal.sdk.domain.facade.DomainEntityBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.DomainEventBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.Repository;
import com.knowis.orderjfinal.sdk.domain.ord.command.OrderCommandBase;
import com.knowis.orderjfinal.sdk.domain.ord.entity.CustomerOrder;
import com.knowis.orderjfinal.sdk.domain.ord.entity.InternalOrder;
import com.knowis.orderjfinal.sdk.domain.ord.entity.Order;
import com.knowis.orderjfinal.sdk.domain.ord.error.OrderCannotBeCancelled;
import com.knowis.orderjfinal.sdk.domain.ord.event.OrderStatusChanged;
import com.knowis.orderjfinal.sdk.domain.ord.type.OrderType;
import com.knowis.orderjfinal.sdk.domain.ord.type.Status;
import com.knowis.orderjfinal.sdk.domain.schemas.TrainingOrderStatusChanged1x1Schema;
// Fundamental 2
import k5.sdk.springboot.domain.events.EventProducerService;
import k5.sdk.springboot.domain.type.Money;
import k5.sdk.springboot.util.text.TextFormatter;
// Fundamental 3
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.Currency;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;List of Imports - File
CustomerOrderCommand.java// Fundamental 1
import com.knowis.orderjfinal.sdk.domain.facade.DomainEntityBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.DomainEventBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.Repository;
import com.knowis.orderjfinal.sdk.domain.ord.command.CustomerOrderCommandBase;
import com.knowis.orderjfinal.sdk.domain.ord.command.OrderCommandBase;
import com.knowis.orderjfinal.sdk.domain.ord.entity.CustomerOrder;
// Fundamental 2
import k5.sdk.springboot.domain.events.EventProducerService;
// Fundamental 3
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;List of Imports - File
InternalOrderCommand.java// Fundamental 1
import com.knowis.orderjfinal.sdk.domain.facade.DomainEntityBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.DomainEventBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.Repository;
import com.knowis.orderjfinal.sdk.domain.ord.command.InternalOrderCommandBase;
import com.knowis.orderjfinal.sdk.domain.ord.command.OrderCommandBase;
import com.knowis.orderjfinal.sdk.domain.ord.entity.InternalOrder;
// Fundamental 2
import k5.sdk.springboot.domain.events.EventProducerService;
//Fundamental 3
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; -
The package name of the provided code is
com.knowis.orderjfinal. Replace all occurrences of this phrase in the files, if the package name of your project is different.
Import Classes - General Approach
-
Importing generated classes based on your implementation design follows this pattern:
import {...} from 'solution-framework/dist/sdk/v1/namespace/*'
Depending on which namespace the class is imported from, the import statements have the following pattern:Namespace-Type Import Pattern API import {...} from 'solution-framework/dist/sdk/v1/namespace/apis/*'Domain Entities: import {...} from 'solution-framework/dist/sdk/v1/namespace/entity/*'Business Errors:import {...} from 'solution-framework/dist/sdk/v1/namespace/error/*'Integration import {...} from 'solution-framework/dist/sdk/v1/namespace/integration' -
Importing generated classes reflecting the capabilities of the IBM DevOps Solution Workbench follows this pattern:
import {...} from 'solution-framework' -
Importing built-in classes in Java does not follow a specific pattern, but here is an example:
import BigNumber from 'bignumber.js' -
Importing classes from other implementation files works via relative paths:
import {...} from '<relative-path-to-file>'
Import Classes for the implemented Commands
-
Since we implemented two commands CreateOrder and CancelOrder, the two corresponding files have to be edited! The following collapsibles show the complete list of imports in all the files.
Compare the list below with your current imports and add the missing classes for each file!
List of Imports - File
CreateOrder.ts// Fundamental 1
import { TrainingOrderStatusChangedV1_1 } from 'solution-framework/dist/sdk/v1/namespace/schema';
// Fundamental 2
import { commands } from 'solution-framework';
// Fundamental 3
import BigNumber from 'bignumber.js';List of Imports - File
CancelOrder.ts// Fundamental 1
import { ord_OrderCannotBeCancelled } from 'solution-framework/dist/sdk/v1/namespace/error/ord_OrderCannotBeCancelled';
import { TrainingOrderStatusChangedV1_1 } from 'solution-framework/dist/sdk/v1/namespace/schema';
// Fundamental 2
import { commands } from 'solution-framework';
You have successfully implemented a Factory and an Instance Command. You have seen how easy it is to implement the logic for creating, persisting and manipulating instances of an entity and for publishing events with the IBM DevOps Solution Workbench. You are now able transform the logic of commands that were designed in the Solution Designer into code on your local machine.
What's Next?โ
In the next course you will learn how you can implement domain services to perform various tasks in your application.