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 theCOMPANYrole.Approve— executed by users with theOFFICERrole.Reject— executed by users with theOFFICERrole.
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:
//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:
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.javaCompanyApproveDto.javaCompanyRejectDto.javaCompanySubmitDto.javaCompanyTypeEntryDto.javaCompanyViewDto.javaShareholderEntryDto.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:
- Initial (NIL)
- Submitted
- Approved
- Rejected
These states are represented using a States enum annotated with @DocumentStates:
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 companyNEW: Start a new company registrationREVIEW: Used by anOFFICERto fetch the document for further actions
Defined under the document module:
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: FromNILtoSUBMITTEDAPPROVE: FromSUBMITTEDtoAPPROVEDREJECT: FromSUBMITTEDtoREJECTED
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, theAPPROVEoperation usesCompanyApproveDto, which only contains the fieldsvatNumberandremark, 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
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, anddocument: These define the identity and endpoint location of the documentversion: Version number of the document definition.stateType: The enum class representing possible document statesstartOperationType: Enum for entry-point operations.operationType: Enum for document state transitions.entityType: The main entity representing the document datasearch: Enables searching on the document using also filtersfeatureFlags: Enables optional features like transactionalityprovider: Defines the persistence provider, here,PanacheProvider(other providers like MongoDB may be supported in the future).
This is how our document folder should look:
