New Entity Guide¶
This guide shows how to add a new entity to an existing module. If you need to create the module first, see the New Module Guide.
Slash Command
You can scaffold an entity automatically with the Claude Code command:
This guide explains the manual process.
What Gets Created¶
Adding one entity produces eight files across three sub-projects:
| Sub-Project | File | Purpose |
|---|---|---|
| Entities | {Entity}.java |
JPA entity |
| DTOs | {Entity}EditDto.java |
Create/update DTO |
| DTOs | {Entity}ListDto.java |
Table display DTO |
| DTOs | {Entity}FilterDto.java |
Filter criteria DTO |
| Service | {Entity}Repository.java |
JPA repository |
| Service | {Entity}EditDtoConverter.java |
Entity-DTO mapping (3 methods) |
| Service | {Entity}ListDtoConverter.java |
Entity-ListDto mapping (1 method) |
| Service | {Entity}Specification.java |
Dynamic query builder |
| Service | {Entity}Service.java |
Business logic + CRUD |
| Service | {Entity}Controller.java |
REST endpoints |
Plus updates to:
docs/database/ddl_scripts.md— New table DDLs_ic{mod}_idtable — New row for ID generationServiceErrorMsgCodes.java— Entity-specific error codesmessages.properties/messages_tr.properties— Error messages
Example: Adding BpmProduct to BPM¶
We will add a BpmProduct entity to the existing ICoSys-BPM module.
Step 1 — Entity¶
ICoSys-BPM-Entities-1.0/.../entity/BpmProduct.java
@Entity
@Table(name = "bpm_product", schema = "icbpm", catalog = "icbpm")
@TableGenerator(
name = "BpmProduct_ID_GEN",
schema = "icbpm", catalog = "icbpm",
table = "s_icbpm_id",
pkColumnName = "table_name",
pkColumnValue = "bpm_product",
valueColumnName = "unique_id",
initialValue = 0, allocationSize = 1
)
@DynamicInsert @DynamicUpdate
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class BpmProduct extends AbstractICEntity6 implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "BpmProduct_ID_GEN")
@Column(name = "id", unique = true, nullable = false)
@EqualsAndHashCode.Include
private Long id;
@Column(name = "product_code", nullable = false, length = 50)
private String productCode;
@Column(name = "product_name", nullable = false, length = 200)
private String productName;
@Column(name = "unit_price")
private BigDecimal unitPrice;
@Column(name = "status")
private Boolean status;
}
Key points:
- Use the existing module's schema and ID table (
icbpm,s_icbpm_id) - Generator name must be unique across the module:
{Entity}_ID_GEN pkColumnValuematches the table name:bpm_product
Step 2 — DTOs¶
BpmProductEditDto.java
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
public class BpmProductEditDto extends ICEntity6Dto {
private String productCode;
private String productName;
private BigDecimal unitPrice;
private Boolean status;
}
BpmProductListDto.java
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
public class BpmProductListDto extends ICEntity6Dto {
private String productCode;
private String productName;
private BigDecimal unitPrice;
private Boolean status;
}
BpmProductFilterDto.java
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor
public class BpmProductFilterDto extends ICEntity6FilterDto {
private Boolean status;
}
Step 3 — Repository¶
BpmProductRepository.java
@Repository
public interface BpmProductRepository extends ICrudRepository<BpmProduct, Long> {
boolean existsByCrProcess_IdAndProductCodeAndIdNot(
Long crProcessId, String productCode, Long id);
}
Custom query methods follow Spring Data naming:
findBy{Field}— select by fieldexistsBy{Field}— existence checkcountBy{Field}— count- Use
CrProcess_Id(underscore) for nested property traversal
Step 4 — Converters¶
Edit Converter (3 Methods)¶
BpmProductEditDtoConverter.java
@Component
public class BpmProductEditDtoConverter
implements IEditDtoConverter<BpmProduct, BpmProductEditDto> {
/** Entity → DTO */
@Override
public BpmProductEditDto from(BpmProduct entity) {
if (entity == null) return null;
BpmProductEditDto dto = BpmProductEditDto.builder()
.productCode(entity.getProductCode())
.productName(entity.getProductName())
.unitPrice(entity.getUnitPrice())
.status(entity.getStatus())
.build();
dto.mapSuperFields(entity); // id, crProcessId, version
return dto;
}
/** DTO → New Entity (create) */
@Override
public BpmProduct to(BpmProductEditDto dto) {
if (dto == null) return null;
return BpmProduct.builder()
.productCode(dto.getProductCode())
.productName(dto.getProductName())
.unitPrice(dto.getUnitPrice())
.status(dto.getStatus())
.build();
// DO NOT set crProcess here — use beforeCreate() hook
}
/** DTO → Existing Entity (update) */
@Override
public BpmProduct to(BpmProductEditDto dto, BpmProduct entity) {
if (dto == null || entity == null) return entity;
if (dto.getVersion() != null) {
entity.setVersion(dto.getVersion()); // Optimistic lock
}
entity.setProductCode(dto.getProductCode());
entity.setProductName(dto.getProductName());
entity.setUnitPrice(dto.getUnitPrice());
entity.setStatus(dto.getStatus());
return entity;
}
}
List Converter (1 Method)¶
BpmProductListDtoConverter.java
@Component
public class BpmProductListDtoConverter
implements IListDtoConverter<BpmProduct, BpmProductListDto> {
@Override
public BpmProductListDto from(BpmProduct entity) {
if (entity == null) return null;
BpmProductListDto dto = BpmProductListDto.builder()
.productCode(entity.getProductCode())
.productName(entity.getProductName())
.unitPrice(entity.getUnitPrice())
.status(entity.getStatus())
.build();
dto.mapSuperFields(entity);
return dto;
}
}
Common Mistake
Forgetting dto.mapSuperFields(entity) causes id, crProcessId, and
version to be null in the response. Always call it in from().
Step 5 — Specification¶
BpmProductSpecification.java
@Component
public class BpmProductSpecification {
private static final String FIELD_ID = "id";
private static final String FIELD_PRODUCT_CODE = "productCode";
private static final String FIELD_PRODUCT_NAME = "productName";
private static final String FIELD_STATUS = "status";
private static final String REL_CR_PROCESS = "crProcess";
/** Builds JPA Specification from filter and search term. */
public Specification<BpmProduct> build(BpmProductFilterDto filter,
String searchTerm) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 1. Tenant isolation (ALWAYS first!)
if (filter != null && filter.getCrProcessId() != null) {
predicates.add(cb.equal(
root.get(REL_CR_PROCESS).get(FIELD_ID),
filter.getCrProcessId()));
}
// 2. Filter predicates
if (filter != null && filter.getStatus() != null) {
predicates.add(cb.equal(
root.get(FIELD_STATUS), filter.getStatus()));
}
// 3. Free-text search
if (StringUtils.hasText(searchTerm)) {
String term = "%" + searchTerm.toLowerCase() + "%";
predicates.add(cb.or(
cb.like(cb.lower(root.get(FIELD_PRODUCT_CODE)), term),
cb.like(cb.lower(root.get(FIELD_PRODUCT_NAME)), term)
));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
Rules:
crProcess.idpredicate is always first — tenant isolation- Use
private static final Stringconstants — no magic strings searchTermusesLIKEwithlower()for case-insensitive search
Step 6 — Service¶
BpmProductService.java
@Service
@Transactional
@Slf4j
public class BpmProductService extends AbstractCrudService<
BpmProduct, Long, BpmProductEditDto,
BpmProductListDto, BpmProductFilterDto> {
private final BpmProductRepository repository;
private final BpmProductEditDtoConverter editConverter;
private final BpmProductListDtoConverter listConverter;
private final BpmProductSpecification specification;
public BpmProductService(
PagingSupport pagingSupport,
BpmProductRepository repository,
BpmProductEditDtoConverter editConverter,
BpmProductListDtoConverter listConverter,
BpmProductSpecification specification) {
super(pagingSupport);
this.repository = repository;
this.editConverter = editConverter;
this.listConverter = listConverter;
this.specification = specification;
}
// --- 5 abstract method implementations ---
@Override
protected ICrudRepository<BpmProduct, Long> getRepository() {
return repository;
}
@Override
protected IEditDtoConverter<BpmProduct, BpmProductEditDto> getEditConverter() {
return editConverter;
}
@Override
protected IListDtoConverter<BpmProduct, BpmProductListDto> getListConverter() {
return listConverter;
}
@Override
protected String getEntityName() { return "BpmProduct"; }
@Override
protected Long extractId(BpmProduct entity) { return entity.getId(); }
@Override
protected Specification<BpmProduct> buildSpecification(
BpmProductFilterDto filter, String searchTerm) {
return specification.build(filter, searchTerm);
}
// --- Lifecycle hooks ---
@Override
protected void beforeCreate(BpmProduct entity, BpmProductEditDto dto) {
if (dto.getCrProcessId() != null) {
CrProcess crProcess = entityManager.getReference(
CrProcess.class, dto.getCrProcessId());
entity.setCrProcess(crProcess);
}
}
@Override
protected void validateForCreate(BpmProductEditDto dto) {
requireNonBlank(dto.getProductCode(), "productCode");
requireNonBlank(dto.getProductName(), "productName");
}
@Override
protected void validateForUpdate(BpmProductEditDto dto, Long id) {
requireNonBlank(dto.getProductCode(), "productCode");
requireNonBlank(dto.getProductName(), "productName");
// Check uniqueness (exclude current record)
if (repository.existsByCrProcess_IdAndProductCodeAndIdNot(
dto.getCrProcessId(), dto.getProductCode(), id)) {
throw new ServiceException(ServiceErrorMsgCodes.PRODUCT_CODE_EXISTS);
}
}
}
Lifecycle Hook Reference¶
| Hook | When | Common Use |
|---|---|---|
validateForCreate(dto) |
Before conversion | Required fields, format checks |
beforeCreate(entity, dto) |
After conversion | Set CrProcess, defaults |
afterCreate(entity) |
After save | Audit log, notifications |
validateForUpdate(dto, id) |
Before fetch | Uniqueness, status checks |
beforeUpdate(entity, dto) |
After merge | Computed fields |
afterUpdate(entity) |
After save | Audit log |
validateDeletion(entity) |
Before delete | Business rules |
checkReferences(id) |
Before delete | FK reference checks |
beforeDelete(entity) |
After validation | Cascade cleanup |
afterDelete(entity) |
After delete | External resource cleanup |
Step 7 — Controller¶
BpmProductController.java
@RestController
@RequestMapping("/api/product")
@RequiredArgsConstructor
public class BpmProductController {
private final BpmProductService service;
@GetMapping("/{id}")
public ResponseEntity<BpmProductEditDto> findById(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<BpmProductEditDto> create(
@RequestBody BpmProductEditDto dto) {
return ResponseEntity.ok(service.create(dto));
}
@PutMapping("/{id}")
public ResponseEntity<BpmProductEditDto> update(
@PathVariable Long id,
@RequestBody BpmProductEditDto dto) {
return ResponseEntity.ok(service.update(id, dto));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/list")
public ResponseEntity<PageResponse<BpmProductListDto>> findPaged(
@RequestBody PageRequest<BpmProductFilterDto> request) {
return ResponseEntity.ok(service.findPaged(request));
}
@GetMapping("/stats")
public ResponseEntity<EntityStatsDto> getStats() {
return ResponseEntity.ok(service.getStats());
}
}
Step 8 — Database¶
Add the table DDL and ID generator row:
Add to docs/database/ddl_scripts.md
INSERT INTO s_icbpm_id (table_name, unique_id) VALUES ('bpm_product', 0);
CREATE TABLE bpm_product (
id BIGINT NOT NULL,
product_code VARCHAR(50) NOT NULL,
product_name VARCHAR(200) NOT NULL,
unit_price DECIMAL(19,4),
status BIT(1),
-- Inherited from AbstractICEntity6
cr_process_id BIGINT NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
xref_id VARCHAR(255),
creation_date DATETIME(6),
created_by VARCHAR(255),
last_update_date DATETIME(6),
last_updated_by VARCHAR(255),
PRIMARY KEY (id),
CONSTRAINT fk_bpm_product_crp
FOREIGN KEY (cr_process_id) REFERENCES icglb2.cr_process(id)
) ENGINE=InnoDB;
Step 9 — Error Codes¶
Add entity-specific codes:
Step 10 — Verify¶
Build and run:
cd ICoSys-BPM
mvn clean install -DskipTests
cd ICoSys-BPM-Services-1.0
mvn spring-boot:run -Dspring-boot.run.profiles=dev
Test:
# Create
curl -X POST http://localhost:8020/icbpm/services/api/product \
-H "Content-Type: application/json" \
-d '{"crProcessId":1,"productCode":"PRD-001","productName":"Widget","unitPrice":29.99,"status":true}'
# List
curl -X POST http://localhost:8020/icbpm/services/api/product/list \
-H "Content-Type: application/json" \
-d '{"page":0,"size":10,"filter":{"crProcessId":1}}'
Entity Checklist¶
- [ ] Entity extends
AbstractICEntity6with@Builder - [ ] EditDto extends
ICEntity6Dto - [ ] FilterDto extends
ICEntity6FilterDto - [ ] Converters call
dto.mapSuperFields(entity)infrom() - [ ] Edit converter sets
versionin update method - [ ] Service implements
beforeCreate()setting CrProcess - [ ] Specification has
crProcess.idas first predicate - [ ] No magic strings in Specification (use constants)
- [ ] Controller has all 6 CRUD endpoints
- [ ] DDL includes all 7 inherited columns
- [ ] ID generator row inserted in
s_ic{mod}_id - [ ] Error codes added to
ServiceErrorMsgCodes - [ ] Messages in both
.propertiesfiles (EN + TR)
What's Next?¶
- CRUD Engine — Deep dive into all lifecycle hooks
- Field Types — Supported field types and rendering
- Deployment — Deploy your changes