What this is about
A hook can do more than set fields. Through the system layer of a model it can write itself: create a log entry, produce a derived record, change another object. That is a second write running in the middle of the first one.
CDMS treats every write as a level of its own. The request’s write is level 1, a write from one of its hooks is level 2, and so on.
Two levels in the flow
Example: a before hook on Order creates a log entry for every new order.
sequenceDiagram
participant C as Client
participant S1 as System layer Order (level 1)
participant H as Before hook
participant S2 as System layer Log (level 2)
participant DB as Database
C->>S1: POST /order/create
S1->>S1: build the graph, collect rule violations
S1->>H: before hooks
H->>S2: createObject(Log)
S2->>S2: own rules, own before hooks, own report
S2->>DB: write Log (no commit)
S2->>S2: own after hooks
S2-->>H: Log
H-->>S1: done
S1->>S1: report own rule violations (422 if any)
S1->>DB: write Order
S1->>S1: own after hooks
S1-->>C: 200
Note over S1,DB: commit at the end of the request, for both
What holds for each level:
| Level 1 (Order) | Level 2 (Log) | |
|---|---|---|
| Rule violations | only its own, reported before Order is written | only its own, reported before Log is written |
| Before hooks | those of Order and its children | those of Log and its children |
| After hooks | run after Order is written | run after Log is written |
| Permissions | class role of Order, field grants of the descent | class role of Log; a field grant from level 1 does not apply here |
| Commit | at the end of the request | none, it belongs to level 1 |
What is stored in the end
| Order | Log entry | Result |
|---|---|---|
| valid | valid | 200, both stored |
| breaks a rule | valid | 422 with the order's violations, nothing stored |
| valid | breaks a rule | 422 with the log entry's violations, nothing stored |
| either | hook throws | response according to the exception, see When a hook fails, nothing stored |
An inner create that asks for createReadMode: LENIENT does not commit either. LENIENT applies only to the request’s write, see Create and read back: STRICT or LENIENT. Otherwise the log entry would be durable before the order has been checked.
How deep it may go
A hook that writes again on every write of its own model would never stop. CDMS therefore bounds the depth:
-
1Client→CDMSPOST /api/rest/order/create
-
2CDMSlevel 1: Order, the before hook creates an Order
-
3CDMSlevel 2, 3, … up to the maximum depth: each Order creates the next one
-
4CDMSThe next level would be beyond the maximum depth
-
5CDMS
HookExecutionExceptionwrite-nesting-too-deep|10→ 500 -
6CDMS→Databasethe whole request is rolled back, not a single Order remains
You set the maximum depth in application.yaml:
codamai:
cdms:
api:
max-write-nesting-depth: 10 # level 1 is the request's write
The response is 500, not 4xx: the client did nothing wrong, the cause is a hook.
In monitoring
If the application has Micrometer and a registry, CDMS measures every request-level write:
| Metric | Kind | Meaning |
|---|---|---|
cdms.write.nesting.depth | distribution | the deepest level a write reached; 1 means no hook wrote |
cdms.write.nesting.refused | counter | writes refused for too much depth |
Both carry the model of the level-1 write as the tag model. A maximum that slowly rises means hooks are calling hooks. Every refusal is also logged as a warning.
Pitfalls
What comes next
- Where hooks sit in the flow: The order within a write operation
- Hooks for children of the same write: Hooks for nested objects and cascades
- What happens when a hook throws: When a hook fails