Unit 12: Implement Domain Service
In this course you will learn how to implement a service within the domain namespace using your local IDE.
Outlineโ
For our next implementation we will stay in the domain namespace and continue with coding a domain service.
As explained in Unit 4: Design Domain Service and Business Error - contrary to a command - a domain service performs logic that is independent of a particular instance and can be used to orchestrate several commands.
Furthermore, domain services can have an input and an output entity and are able to publish events and throw business errors.
Since services are part of the whole domain and do not belong to a root entity, all service classes are located in the service folder of the domain.
The design of domain services modeled in the Solution Designer is reflected in the service classes by its fields, method signatures and available scope.
In general, the implementation of domain services does follow a pattern like for commands, but the following pieces may be part of it:
- Retrieve the service input.
- Manipulate multiple entites either directly or via orchestrating commands for them. If something goes wrong or a condition is not met, throw a business error.
- Publish a business event.
- Return or assign the service output.
Prerequisitesโ
- You have successfully completed Unit 10: Set up Project for Implementation.
- You have successfully completed Unit 4: Design Domain Service and Business Error.
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: 15 minutes
Exercise goal: After completing this course you are able to apply all pieces of implementing a domain service.
Supported languages: Java and TypeScript
In this exercise we will implement a domain service CancelAllOrdersOfCustomer which cancels all orders of a specific customer that have not been processed yet.
Since the service is only responsible for customer orders, the cancellation will only affect CustomerOrder root entities.
Step 1: Implement the domain serviceโ
Our domain service to cancel all orders of a customer will include the following:
- Extract all orders with the provided customer reference id that have status "OPEN" or "IN_PROGRESS".
- If no matching orders were found, throw a Business Error.
- Call the
CancelOrdercommand for each of the matching orders.
- Java
- TypeScript
-
Open the file
/src/main/java/<package-name>/domain/ord/service/CancelAllOrdersOfCustomer.java.
You will see an auto-generated stub in which you can start your implementation.<package-name>: The package name of the Java project (e.g.com.knowis.orderjfinal)@Service("ord_CancelAllOrdersOfCustomer")
public class CancelAllOrdersOfCustomer extends CancelAllOrdersOfCustomerBase {
private static final Logger log = LoggerFactory.getLogger(CancelAllOrdersOfCustomer.class);
public CancelAllOrdersOfCustomer(DomainEntityBuilder entityBuilder, Repository repo) {
super(entityBuilder, repo);
}
@NewSpan
@Override
public void execute(
CancelAllOrdersOfCustomerInput cancelAllOrdersOfCustomerInput
) throws NoMatchingOrdersFound {
log.info("CancelAllOrdersOfCustomer.execute()");
// TODO: Add your service implementation logic
}
} -
To reuse the implemented command
CancelOrderas well as theCustomOrderRepositoryclass we added manually in preparation for the implementation phase, we have to create a private field for classOrderCommandand for the repository class. Additionally we have to add two constructor parameters that are then assigned to the respective private fields.private final CustomOrderRepository customOrderRepo;
private final OrderCommand orderCommand;
public CancelAllOrdersOfCustomer(
DomainEntityBuilder entityBuilder,
Repository repo,
CustomOrderRepository customOrderRepo,
OrderCommand orderCommand
) {
super(entityBuilder, repo);
this.customOrderRepo = customOrderRepo;
this.orderCommand = orderCommand;
}Please use the auto-import function of your IDE to import the missing classes (and repeat this for the following steps). If you are not very familiar with imports, go back to Course 11 - Step 3: Import missing Classes for detailed information on class imports. Furthermore, you are free to compare your current imports with the recommended list of imports at the end of this section. Therefore, click here.
-
After injecting the fields, we will extend the
executemethod in the following steps.
First, we will extract all orders with the provided customer reference id that have status "OPEN" or "IN_PROGRESS". Therefore we will use theCustomOrderRepositoryclass.// Step 1: Find all customer orders matching the referenceId and status OPEN or IN_PROGRESS
// Therefore use the filter function from the manually provided class CustomOrderRepository
List<CustomerOrder> matchingOrders = this.customOrderRepo.findCustomerOrdersByStatus(
cancelAllOrdersOfCustomerInput.getCustomerReferenceId(),
List.of(Status.OPEN, Status.IN_PROGRESS)
);โ๏ธinfoIf you want a detailed description on how to extend repository classes for advanced filtering on your own, check out How-To: Extend MongoDB Repositories for advanced filtering.
-
If no matching orders exist, we will throw a business error.
// Step 2: If no matching orders found (empty list) throw a business error
if (matchingOrders.isEmpty()) {
throw new NoMatchingOrdersFound();
} -
After this, we have to loop over all matching orders and use the
OrderCommandclass to cancel them one by one.// Step 3: For all matching orders execute the 'CancelOrder' command
for (CustomerOrder order : matchingOrders) {
try {
this.orderCommand.cancelOrder(order);
} catch (OrderCannotBeCancelled e) {
log.error(
TextFormatter.format(
"Cannot cancel order of customer reference ID = {}, order ID = {}",
cancelAllOrdersOfCustomerInput.getCustomerReferenceId(),
order.getId()
),
e
);
}
}
The provided list supports you to compare your current imports with the recommended list, especially if your IDE complains about errors in the code at this stage of the course.
List of Imports - File CancelAllOrdersOfCustomer.java
// Fundamental 1: Generated classes based on design
import com.knowis.orderjfinal.sdk.domain.facade.DomainEntityBuilder;
import com.knowis.orderjfinal.sdk.domain.facade.Repository;
import com.knowis.orderjfinal.sdk.domain.ord.entity.CustomerOrder;
import com.knowis.orderjfinal.sdk.domain.ord.error.NoMatchingOrdersFound;
import com.knowis.orderjfinal.sdk.domain.ord.error.OrderCannotBeCancelled;
import com.knowis.orderjfinal.sdk.domain.ord.service.CancelAllOrdersOfCustomerBase;
import com.knowis.orderjfinal.sdk.domain.ord.type.Status;
// Fundamental 2: Generated classes reflecting the capabilities of the Workbench
import k5.sdk.springboot.util.text.TextFormatter;
// Fundamental 3: Built-in Java classes
import io.micrometer.tracing.annotation.NewSpan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
// Fundamental 4: Classes from other implementation files
import com.knowis.orderjfinal.domain.ord.command.OrderCommand;
import com.knowis.orderjfinal.domain.ord.repository.CustomOrderRepository;
The package name of the provided code is com.knowis.orderjfinal. All occurrences of this phrase have to be replaced with the package name of your project.
For this service we have not defined an output entity, so we stop with the implementation here.
But if an output is defined in your own domain service, this is simply applied in implementation by a return-statement.
-
Open the file
/src-impl/domain/ord/services/CancelAllOrdersOfCustomer.ts.
You will see an auto-generated stub with a methodexecutein which we will provide the implementation of the domain service.export default class extends services.ord_CancelAllOrdersOfCustomer {
public async execute(): Promise<void> {
const log = this.util.log;
log.debug('ord_CancelAllOrdersOfCustomer.execute()');
}
} -
First, we will extract all orders with the provided customer reference id that have status "OPEN" or "IN_PROGRESS". Therefore we will use the built-in filter expressions of the IBM DevOps Solution Workbench.
// Step 1: Find all customer orders matching the customerReferenceId and status OPEN or IN_PROGRESS
// Use the filter expressions provided by the k5 SDK
const referenceIdFilter = this.factory.filter.ord.CustomerOrder.customerReferenceId.comparison('=eq=', this.input.customerReferenceId);
const statusOpenFilter = this.factory.filter.ord.CustomerOrder.status.comparison('=eq=', 'OPEN');
const statusInProgressFilter = this.factory.filter.ord.CustomerOrder.status.comparison('=eq=', 'IN_PROGRESS');
const statusFilter = this.factory.filter._or(statusOpenFilter, statusInProgressFilter);
const combinedFilter = this.factory.filter._and(referenceIdFilter, statusFilter);
const orders = await this.repo.ord.CustomerOrder.find({ includeSubEntities: false }, combinedFilter);this.factory.filter: provides access to the built-in filter expressionsthis.repo.CustomerOrder.find(): finds instances of classCustomerOrderin the database. With parameterfilterit will only return the entities where the filter expression matches. This is an asynchronous operation and therefore needs to be awaitedโ๏ธinfoIf you want a detailed description on the built-in filter expressions in the IBM DevOps Solution Workbench, check out Product Documentation - Persist Data.
-
If no matching orders exist, we will throw a business error.
// Step 2: If no matching orders found (empty list) throw a business error
if (!orders) {
throw ord_NoMatchingOrdersFound;
}Please use the auto-import function of your IDE to import the missing classes (and repeat this for the following steps). If you are not very familiar with imports, go back to Course 11 - Step 3: Import missing Classes for detailed information on class imports. Furthermore, you are free to compare your current imports with the recommended list of imports at the end of this section. Therefore, click here.
-
After this, we have to loop over all matching orders and cancel them one by one.
// Step 3: For all matching orders execute the 'CancelOrder' command
for (const order of orders) {
try {
await order.CancelOrder();
} catch (e) {
log.error('Cannot cancel order of customer reference ID: ' + this.input.customerReferenceId + ', order ID: ' + order._id, e);
}
}order.CancelOrder(): executes the Instance CommandCancelOrderfor an instance of root entityOrder. This is an asynchronous operation and therefore needs to be awaited
The provided list supports you to compare your current imports with the recommended list, especially if your IDE complains about errors in the code at this stage of the course.
List of Imports - File CancelAllOrdersOfCustomer.ts
// Fundamental 1: Generated classes based on design
import { ord_NoMatchingOrdersFound } from 'solution-framework/dist/sdk/v1/namespace/error/ord_NoMatchingOrdersFound';
// Fundamental 2: Generated classes reflecting the capabilities of the Workbench
import { services } from 'solution-framework';
For this service we have not defined an output entity, so we stop with the implementation here.
But if necessary, the field this.output, which provides type-safe access to the output that is defined in the Solution Designer, has to be assigned with a value.
You have successfully implemented a domain service! In combination with the design in the Solution Designer, you are able to use domain services in your own application for various purposes, as they are not bound to a specific instance of a root entity.
What's Next?โ
In the next course you will learn how to implement the agent that gets triggered when the status of a customer changes.