Skip to main content

Unit 11: Implement Commands and Publish Events

๐ŸŽฏOverview

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:

  1. Based on the command type, create an instance or manipulate it. If the conditions are not met to do so, throw a business error.
  2. Persist the changes of the instance in the database.
  3. Publish an event to communicate the changes with other services.

Prerequisitesโ€‹

โ—๏ธAlternative starting point for your training

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
  1. 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 class OrderCommand and an empty method createOrder. 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;
    }

    ...
    }
  2. 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 newInstance

    createOrderInput: provides type-safe access to the properties modeled in the input entity of the command

    this.entityBuilder.getOrd(): provides the constructors to create each kind of entity in domain namespace ord

  3. 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.

  4. 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 design

    TrainingOrderStatusChanged1x1Schema: represents the event schema modeled in the schema registry of the Solution Designer

    this.eventProducer.publish(event): publishes the created event

  5. Return the order instance, as factory commands always have to return the persisted instance.

    // Step 4: Return the persisted instance
    return persistedInstance;
  6. The root entity Order has a child CustomerOrder. 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 method createOrder, 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 CreateOrder does not differ in the children of root entity Order, you can use the command of the parent. Replace null with (CustomerOrder) super.createOrder(createOrderInput), so that the method createOrder looks 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);
    }
  7. Open the file /src/main/java/<package-name>/domain/ord/command/InternalOrderCommand.java and repeat step 6 for root entity InternalOrder. Replace (CustomerOrder) in the return statement with (InternalOrder).

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
  1. Open the file /src/main/java/<package-name>/domain/ord/command/OrderCommand.java.
    You will find the empty method cancelOrder with 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 CancelOrder command. Therefore, we switch into the cancelOrder method and extend it with the code in the following steps.

  2. 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 executed

    throw new OrderCannotBeCancelled(): throws the business error which has been added to the command in the design

  3. 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);
  4. Then we have to persist the instance.

    // Step 3: Persist the updated instance
    Order persistedInstance = this.repo.getOrd().getOrder().save(instance);
  5. 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()
    ));
  6. The root entity Order has a child CustomerOrder. 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 method cancelOrder, 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 CancelOrder does not differ in the children of root entity Order, you can use the command of the parent. Add the line super.cancelOrder(instance), so that the method cancelOrder looks 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);
    }
  7. Open the file /src/main/java/<package-name>/domain/ord/command/InternalOrderCommand.java and repeat step 6 for root entity InternalOrder.

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:

  1. 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
  2. Generated classes reflecting the capabilities of the IBM DevOps Solution Workbench (These classes are available in each implementation project)
  3. Built-in classes based on the framework of your selected implementation language
  4. 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.

๐Ÿ’กAuto-Import Classes with your IDE

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.

Import Classes - General Approach

  1. 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-TypeImport Pattern
    APIimport <package-name>.sdk.api.*
    Domainimport <package-name>.sdk.domain.*
    Integrationimport <package-name>.sdk.integration.*
  2. Importing generated classes reflecting the capabilities of the IBM DevOps Solution Workbench follows this pattern: import k5.sdk.springboot.*

  3. 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;
  4. 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

  1. 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;
  2. 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.

๐ŸŒŸCongratulations!

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.