Skip to main content

Workflow: Defining States and Operations

This section focuses on how to define states and operations in our application.

Just like the data model structure, the possible states and actions of an entity must be well-defined from the beginning. To manage this, we use annotations provided by SOKit.

Use Case Actions

In this tutorial example, we define four actions related to the CompanyEntity:

  • View — available to all users.
  • Submit — executed by users with the COMPANY role.
  • Approve — executed by users with the OFFICER role.
  • Reject — executed by users with the OFFICER role.

We will leverage SOKit to generate the necessary DTOs for these operations.

Annotating DTOs

Still within the data module, we annotate the CompanyEntity with @Dto for each of the actions:

CompanyEntity.java
//other imports here ...
import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;

@Dto(variant = "View", altVariants = "Entry")
@Dto(variant = "Submit", altVariants = "Entry")
@Dto(variant = "Approve", altVariants = "Entry")
@Dto(variant = "Reject", altVariants = "Entry")
public class CompanyEntity extends DocumentEntity implements Serializable {..}

Indeed, Sokit provides a powerful and declarative way to generate DTOs (Data Transfer Objects) directly from your entity classes using the @Dto annotation. This feature is part of the sokit-dm extension and is especially useful when you need to define different data shapes for operations such as create, update, view, or search—without having to manually create separate DTO classes.

@Dto Annotation

The @Dto annotation allows you to specify, directly within your entity class, which DTOs should be generated for each operation type.

In the code snippet you can notice the variant attribute which specifies the primary DTO to be generated for the entity for a specific operation. The altVariants attribute defines one or more fallback DTO variants to use when a specific variant is not available for a nested object during DTO generation.

Field-Level Inclusions

Since we only want specific fields included in the Approve and Reject DTOs, we use the @Dto.Include annotation. The includeAll = false flag tells SOKit to exclude all fields unless explicitly included.

  @Dto.Include({"Approve", "Reject"})
private String vatNumber;

@Dto.Include({"Approve", "Reject"})
private String remark;

This means vatNumber and remark will be included in both Approve and Reject DTOs.

The entity now becomes:

CompanyEntity.java
package com.dev.registration.company.data;

import jakarta.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;
import com.strategyobject.sokit.extensions.document.api.entities.DocumentEntity;

@Entity
@Table(name = "TBL_COMPANY")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Dto(variant = "View", altVariants = "Entry")
@Dto(variant = "Submit", altVariants = "Entry")
@Dto(variant = "Approve", altVariants = "Entry", includeAll = false)
@Dto(variant = "Reject", altVariants = "Entry", includeAll = false)
public class CompanyEntity extends DocumentEntity implements Serializable {

// Fields...
@Dto.Include({"Approve", "Reject"})
private String vatNumber;

@Dto.Include({"Approve", "Reject"})
private String remark;
}

Other DTOs

We apply the same logic to the other entity and embedded classes:

//other imports here ...
import com.strategyobject.sokit.extensions.dm.api.annotations.Dto;

@Entity
@Table(name = "TBL_SHAREHOLDER")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Dto(variant = "Entry")
public class ShareholderEntity extends SubElement implements Serializable {
// ...
}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Embeddable
@Dto(variant = "Entry")
public class CompanyType implements Serializable {
// ...
}

@Dto(variant = "Entry")
public class CompanyAddress implements Serializable {
// ...
}

Building the Project

Now we run the following command in the terminal to build the project and generate the DTOs:

./gradlew clean build

The generated DTOs will be available under the following path:

com/company-registration-system/data/build/generated/sources/annotationProcessor/java/main/com/dev/registration/company/data/dm

You will find these 7 auto-generated DTO files:

  • CompanyAddressEntryDto.java
  • CompanyApproveDto.java
  • CompanyRejectDto.java
  • CompanySubmitDto.java
  • CompanyTypeEntryDto.java
  • CompanyViewDto.java
  • ShareholderEntryDto.java

These are the DTOs we will use in our Document layer and business logic.

Defining Document States and Operations

To manage a company registration workflow, we must define the states that the document can take and the operations that can be performed throughout its lifecycle.

Document States

The lifecycle of a company registration involves the following states:

  1. Initial (NIL)
  2. Submitted
  3. Approved
  4. Rejected

These states are represented using a States enum annotated with @DocumentStates:

States.java
package com.dev.registration.company.document;

import com.strategyobject.sokit.extensions.document.api.annotations.DocumentStates;
import com.strategyobject.sokit.extensions.document.api.annotations.InitialState;
import com.strategyobject.sokit.extensions.document.api.annotations.State;

@DocumentStates
public enum States {
@InitialState
@State(name = "Initial", description = "Initial State")
NIL,

@State(name = "Submitted", description = "Company Submitted")
SUBMITTED,

@State(name = "Approved", description = "Company Approved")
APPROVED,

@State(name = "Rejected", description = "Company Rejected")
REJECTED,
}

Start Operations

Start operations are the entry points for interacting with a document. In our use case, we define the following:

  • VIEW: View a created company
  • NEW: Start a new company registration
  • REVIEW: Used by an OFFICER to fetch the document for further actions

Defined under the document module:

StartOperations.java
package com.dev.registration.company.document;

import com.dev.registration.company.data.dm.CompanyViewDto;

import com.strategyobject.sokit.extensions.document.api.annotations.DocumentStartOperations;
import com.strategyobject.sokit.extensions.document.api.annotations.StartOperation;
import com.strategyobject.sokit.extensions.document.api.types.StartOperationType;

@DocumentStartOperations
public enum StartOperations {
@StartOperation(
ordinal = 1,
type = StartOperationType.READ,
summary = "View Company",
description = "View Company",
outputType = CompanyViewDto.class)
VIEW,

@StartOperation(
ordinal = 1,
type = StartOperationType.CREATE,
summary = "New Company",
description = "New Company",
outputType = CompanyViewDto.class)
NEW,

@StartOperation(
ordinal = 2,
type = StartOperationType.UPDATE,
summary = "Review Company",
description = "Review Company",
outputType = CompanyViewDto.class)
REVIEW,
}

As you can see, we specify the output DTO for each operation. These DTOs were previously auto-generated using SOKit.

Document Operations

Operations are the actions that transition the document from one state to another. These include:

  • SUBMIT: From NIL to SUBMITTED
  • APPROVE: From SUBMITTED to APPROVED
  • REJECT: From SUBMITTED to REJECTED
Operations.java
package com.dev.registration.company.document;

import com.dev.registration.company.data.dm.CompanyApproveDto;
import com.dev.registration.company.data.dm.CompanyRejectDto;
import com.dev.registration.company.data.dm.CompanySubmitDto;

import com.strategyobject.sokit.extensions.document.api.annotations.DocumentOperations;
import com.strategyobject.sokit.extensions.document.api.annotations.Operation;

@DocumentOperations
public enum Operations {
@Operation(
ordinal = 1,
summary = "Create Company",
description = "Create Company",
startState = "NIL",
endState = "SUBMITTED",
startOperation = "NEW",
inputType = CompanySubmitDto.class)
SUBMIT,

@Operation(
ordinal = 1,
summary = "Approve Company",
description = "Approve Company",
startState = "SUBMITTED",
endState = "APPROVED",
startOperation = "REVIEW",
inputType = CompanyApproveDto.class)
APPROVE,

@Operation(
ordinal = 2,
summary = "Reject Company",
description = "Reject Company",
startState = "SUBMITTED",
endState = "REJECTED",
startOperation = "REVIEW",
inputType = CompanyRejectDto.class)
REJECT,
}

As shown above, each operation includes an inputType, referring to the specific DTO required to execute the operation. For example, the APPROVE operation uses CompanyApproveDto, which only contains the fields vatNumber and remark, as defined in the DTO section earlier.

Defining the Document Interface

The final step is to define the interface of our registration document using the @Document annotation provided by SOKit.

This interface brings together everything we’ve configured so far(the entity, states, operations, and search functionality) into a single contract.

Interface Definition

CompanyDocument.java
package com.dev.registration.company.document;

import com.dev.registration.company.data.CompanyEntity;
import com.dev.registration.company.data.dm.CompanySearchableDto;
import com.dev.registration.company.document.filters.DefaultFilter;

import com.strategyobject.sokit.extensions.core.annotations.FeatureFlag;
import com.strategyobject.sokit.extensions.document.api.annotations.Document;
import com.strategyobject.sokit.extensions.document.providers.panache.PanacheProvider;
import com.strategyobject.sokit.extensions.qdsl.annotations.Search;

@Document(
name = "Company",
description = "Company Document",
module = "registration",
document = "company",
version = 1,
stateType = States.class,
startOperationType = StartOperations.class,
operationType = Operations.class,
entityType = CompanyEntity.class,
search = @Search(
description = DefaultFilter.DESCRIPTION,
projectionType = CompanySearchableDto.class,
filter = DefaultFilter.class),
featureFlags = @FeatureFlag(key = "transactional"),
provider = PanacheProvider.class)
public interface CompanyDocument {}

In this interface, we define:

  • name, description, module, and document: These define the identity and endpoint location of the document
  • version: Version number of the document definition.
  • stateType: The enum class representing possible document states
  • startOperationType: Enum for entry-point operations.
  • operationType: Enum for document state transitions.
  • entityType: The main entity representing the document data
  • search: Enables searching on the document using also filters
  • featureFlags: Enables optional features like transactionality
  • provider: Defines the persistence provider, here, PanacheProvider (other providers like MongoDB may be supported in the future).

This is how our document folder should look:

Document Folder