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 logic in the owned service

The model says what the application is. What it cannot say goes into files the generator hands over to you - first of all the extension service XxxService, which extends the regenerated XxxCoreService. Its second line starts with // Initially generated by ElastiCORE;: it is yours once you edit it.

WhatWhere
a rule about writes - a value worked out from others, a check against other rowsbeforeSave in XxxService
a sign-in rule (locked accounts)override authenticate(login, password)
more work in a lifecycle step (a courier call)override the command in XxxService (ship(orderId))
a rule inside the aggregate (a state check, an event)the behaviour class Xxx of a split root
an operation the CRUD does not have (return a loan, a report)a method in XxxService + one small controller of yours
a custom query in a generated repositoryone preserved block

Do not write controllers, DTOs, repositories, security, sign-in or error handlers by hand: they are generated. Add methods to the generated XxxService.java after the first generation - do not write that file before it exists.

beforeSave - every write of a row​

The core service calls it after the request's values are on the entity, just before saving - on create, PUT /, PUT /batch and update by condition.

HookWhen
beforeSave(entity)what the entity alone decides: recompute a total, stamp a state
beforeSave(entity, request)what depends on what the request carried

The default beforeSave(entity, request) calls beforeSave(entity), so override one of them.

The pet store places a new order before it is first saved:

application/PurchaseOrderService.java (the pet store)
@Override
protected void beforeSave(PurchaseOrder order) {
if (order.getOrderDate() == null) {
order.place(LocalDate.now(clock), this::homeOf, vatRate, discountRate);
}
}

and the shop policy itself lives on the root:

domain/model/PurchaseOrder.java (the pet store, abridged)
public void place(LocalDate today, Function<Long, Address> homeOf, BigDecimal vatRate, BigDecimal discountRate) {
if (getOrderDate() != null) throw new InvariantViolationException("order " + getOrderId() + " is already placed");
if (getDeliveryAddress() == null) setDeliveryAddress(homeOf.apply(getCustomerId()));
// ...

Money withoutVat = getTotalWithoutVat(); // the lines' @compute sum
Money vat = percent(withoutVat, vatRate);
Money discount = percent(withoutVat.plus(vat), discountRate);

setOrderDate(today);
setVat(vat);
setDiscount(discount);
setTotal(withoutVat.plus(vat).minus(discount).rounded(2));
raisePlaced();
}

"Only the first time" is told apart by a state - an id that is still null with @sequence, an empty date as above. For a time set when the row is made, you need no code: @readonly @default(now).

Refusing​

Throw the runtime's exceptions; the generated error handler answers them with your message as detail - see API and error answers:

throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn);   // 409
throw new BadRequestException("..."); // 400

entityManager is a field of the service: entityManager.find(Book.class, isbn) reads a row, entityManager.createQuery("select count(l) from Loan l where ...", Long.class) counts.

Overriding update() - and PUT /batch​

From 3.0.261022-rc1 batchUpdate (PUT /batch) calls update(dto) for each row in one transaction, so a rule in an override of update() holds for batches too, with the same checks and 409 translation. Before, the batch copied update()'s body and skipped such overrides. A rule about writes still belongs in beforeSave, which every path passes through.

An operation of your own​

A method in the service plus one small controller. From the agent kit's shop recipe - an administrator's report:

application/PurchaseOrderService.java - added
    /** Orders per status and the revenue of those not cancelled. "report" is not in the entity's
* @access, so it is the administrator's: 401 for a visitor, 403 for a member. */
@org.springframework.transaction.annotation.Transactional(readOnly = true)
public java.util.Map<String, Object> report() {
ACCESS.require(currentUser, "report");
java.util.Map<String, Long> counts = new java.util.LinkedHashMap<>();
java.math.BigDecimal revenue = java.math.BigDecimal.ZERO;
for (Object[] row : entityManager.createQuery(
"select o.status, count(o), coalesce(sum(o.total), 0) from PurchaseOrder o group by o.status",
Object[].class).getResultList()) {
com.acme.store.domain.model.OrderStatus status = (com.acme.store.domain.model.OrderStatus) row[0];
counts.put(status.name(), (Long) row[1]);
if (status != com.acme.store.domain.model.OrderStatus.CANCELLED) {
revenue = revenue.add(new java.math.BigDecimal(String.valueOf(row[2])));
}
}
java.util.Map<String, Object> report = new java.util.LinkedHashMap<>();
report.put("countByStatus", counts);
report.put("revenue", revenue.setScale(2, java.math.RoundingMode.HALF_UP));
return report;
}
adapter/in/web/OrderReportController.java - a new file of yours
@RestController
@RequestMapping("/api/store/purchaseOrder")
public class OrderReportController {

private final PurchaseOrderService service;

public OrderReportController(PurchaseOrderService service) {
this.service = service;
}

@GetMapping("/report")
public Map<String, Object> report() {
return service.report();
}
}

ACCESS (the entity's @access), currentUser and entityManager are fields of the generated service. Put the controller in the package of the generated controllers so the generated error handler answers its 401 and 403. The next build reports the service SKIP(user-owned) and keeps the method.

In the root: split roots (DDD bundles)​

A root's lifecycle steps are generated; more work goes in your behaviour class with super:

public class PurchaseOrder extends PurchaseOrderBase {
@Override
public void ship() {
super.ship(); // state check + move + event
// then the rest
}
}

A command in commands: without a lifecycle is an abstract method on the base: the build does not compile until you write it in the behaviour class.

Other files you own​

  • Controllers - generate-once. An edited controller does not receive new endpoints (a new command, @signin); the build warns which are missing.
  • XxxApiErrors - the error answers' shape.
  • Domain services, process managers, factories - their bases are regenerated, your class is not.
  • The application class, start-up data, event listeners - ordinary code of yours.