Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

Your first ruleset

Attach a team rules module, ps-rules, to the pet store project (hexagonal, Java). By the end of this page every aggregate root is generated with jMolecules' @AggregateRoot, and every identifier field with @Identity.

1. Rules live in a separate module​

./gradlew build generates code before it compiles the application, so the generator cannot see rules inside the application's sources. Rules go in a separate module that is compiled first.

elasticore-petstore/
├── settings.gradle ← include 'ps-rules'
├── build.gradle ← connects the rules to the generator
├── ps-rules/ ← the new module (any name)
│ ├── build.gradle
│ └── src/
│ ├── main/java/com/example/rules/PetstoreRules.java
│ ├── main/resources/elasticore-template/v3/ ← skeletons for new files (chapter 5)
│ └── test/java/com/example/rules/PetstoreRulesTest.java
└── src/ … ← the application
settings.gradle
include 'ps-rules'
ps-rules/build.gradle
plugins {
id 'java-library'
}

java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }

repositories {
mavenCentral()
}

dependencies {
// The generator runs these rules; it is not part of the application.
// The same version as the io.elasticore plugin.
compileOnly 'io.elasticore:elasticore-base:3.0.261022-rc1'

testImplementation 'io.elasticore:elasticore-base:3.0.261022-rc1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2'
}

tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
tasks.named('test') { useJUnitPlatform() }

Write the same elasticore-base version as the plugin (id 'io.elasticore' version '…'). If the API the rules were compiled against differs from the generator's, generation fails with NoSuchMethodError. Why compileOnly: at run time the generator brings its own elasticore-base.

2. Connecting it to the generator​

build.gradle
elasticore {
architecture = 'hexagonal'
language = 'java'
springBoot = '4'
rules = ['ossie-export', 'com.example.rules.PetstoreRules'] // added after the bundle
}

dependencies {
// The team rules: on the generator's classpath only, not in the application.
elasticore project(':ps-rules')
// What the team rules put on the classes (@AggregateRoot, @ValueObject, @Identity).
implementation 'org.jmolecules:jmolecules-ddd:1.10.0'
// ... the existing dependencies as they are
}
LineMeans
rules = [...]The full names of the ruleset classes. They are added after the bundle that architecture and language chose - do not write the bundle again. The pet store already uses ossie-export, so the class goes after it
elasticore project(':ps-rules')elasticore is the generator's classpath. It does not go into the application jar, and this module is compiled before generation
jmolecules-dddThe annotations the rules add are needed when the application compiles

Always set architecture as well. Without it the 2.x-compatible generator runs and the rule engine does not.

3. The first rules​

ps-rules/src/main/java/com/example/rules/PetstoreRules.java
package com.example.rules;

import io.elasticore.base.model.pub.rules.AnnoCode;
import io.elasticore.base.model.pub.rules.CodeRules;
import io.elasticore.base.model.pub.rules.EntitySpec;
import io.elasticore.base.model.pub.rules.FieldSpec;
import io.elasticore.base.model.pub.rules.Rule;

import static io.elasticore.base.model.pub.rules.Ctx.ctx;

/** The pet store team's rules for the code ElastiCORE generates. */
public class PetstoreRules implements CodeRules {

private static final String JMOLECULES = "org.jmolecules.ddd.annotation.%s";

/** A root gets @AggregateRoot, an @embeddable gets @ValueObject; an @abstract base class gets neither. */
@Rule("ps-aggregate-root")
void aggregateRoot(EntitySpec e) {
if (e.hasMeta("abstract")) return;
if (e.hasMeta("embeddable")) {
ctx(e).ensureClassAnnotation(AnnoCode.of(JMOLECULES, "ValueObject"));
} else if (e.aggregateRoot()) {
ctx(e).ensureClassAnnotation(AnnoCode.of(JMOLECULES, "AggregateRoot"));
}
}

/** Every @id field gets @Identity. */
@Rule("ps-identity")
void identity(EntitySpec e, FieldSpec f) {
if (f.hasDsl("id")) {
ctx(e).field(f).ensure(AnnoCode.of(JMOLECULES, "Identity"));
}
}
}
CodeMeans
implements CodeRulesA marker interface with no methods to implement. The class must be public and have a no-argument constructor
@Rule("ps-aggregate-root")This method is a rule. The value is the rule id - use a team prefix (ps-) so it never clashes with a default rule
(EntitySpec e)Called once per entity file
(EntitySpec e, FieldSpec f)Called once per field
e.aggregateRoot(), e.hasMeta("embeddable")Read @root and @embeddable from the YAML
ctx(e)The file being made
ensureClassAnnotation(...)A class annotation. Never added twice if already there; the import is automatic
ctx(e).field(f).ensure(...)An annotation on that field
AnnoCode.of(JMOLECULES, "Identity")The annotation to add - its full name (%s formatting allowed)

No target is given, so both rules contribute to the entity file (entity).

4. Checking that it is connected​

./gradlew elcore3 -PlistRules

This makes no files; it shows, per domain, the rules that run now, in the order they run. What the pet store actually printed (in part):

[rules] petstore: hexagonal-springboot + ossie-export + com.example.rules.PetstoreRules
entity
0 head access-signin-check ServiceAccessRules
3 head ddd-reference-context DddReferenceRules
5 head v3-class-annotations JpaEntityAnnotationRules
6 head v3-lombok JpaEntityFileRules
7 head v3-auditing-listener JpaEntityAnnotationRules
8 head ddd-behavior-base DddBehaviorRules
0 head ps-aggregate-root PetstoreRules
5 field jpa-field-desc JpaEntityBaseRules
10 field jpa-id JpaEntityBaseRules
…
71 field ddd-state-initial DddStateRules
0 field ps-identity PetstoreRules
80 tail ddd-value-object DddValueObjectRules
82 tail ddd-reference DddReferenceRules
  • If the first line shows com.example.rules.PetstoreRules, it is connected.
  • Even with order 0, our rules run in each phase (head, field) after all of ElastiCORE's rules. So they can remove what a default rule added, or add on top of it.
  • The columns: target (entity) · order · phase (head/field/tail, once for domain and enum rules) · rule id · class.

5. The generated code​

./gradlew build

Customer.java (a generator's file, rewritten every time):

@EntityListeners(AuditingEntityListener.class)
@AggregateRoot // ps-aggregate-root
public class Customer implements java.io.Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "customer_id")
@Identity // ps-identity
private Long customerId;

The generator also adds import org.jmolecules.ddd.annotation.AggregateRoot;. Address (@embeddable) gets @ValueObject.

A developer's file gets a notice​

PurchaseOrder is a root split into two files. PurchaseOrderBase.java, which holds the fields, is the generator's, so the rules apply to it as usual; but PurchaseOrder.java, where the business logic goes, is the developer's, and the generator does not rewrite it. Instead, the generation log points out the missing class annotations:

    behavior     src/main/java/io/elasticore/sample/petstore/domain/model/PurchaseOrder.java  SKIP(user-owned)
[elcore3] src/main/java/io/elasticore/sample/petstore/domain/model/PurchaseOrder.java: the rules also put these on the class, which this file does not have - it is yours, so add them by hand if they apply:
@AggregateRoot

Add it by hand once, as the notice says. Roots created after the rule is in place get it from the start.

src/main/java/io/elasticore/sample/petstore/domain/model/PurchaseOrder.java
import org.jmolecules.ddd.annotation.AggregateRoot;

@AggregateRoot
@Entity
@Table(name = "ps_purchase_order")
public class PurchaseOrder extends PurchaseOrderBase {

Next​