What this is about
On a read, only what comes back is decided. On a write, what ends up in the database is decided — and for that, noticeably more stations run, in a fixed order. This page plays a write through from the “Save” button to the commit and says at every point who is checking and what happens on a refusal.
The read path sits next to it on From login to the data. The first half is the same; this page starts where they part.
The whole path in one picture
sequenceDiagram
autonumber
participant U as User
participant F as BFF
participant FK as Filter chain (CIAS)
participant R as REST layer (CDMS)
participant S as System layer (CDMS)
participant H as Hook
participant DB as Database
U->>F: clicks "Save"
F->>F: decrypt the cookie, get the access token (renew if needed)
F->>FK: PUT /api/rest/crm/customer/update/42<br/>bearer token, data + response
FK->>FK: check and exchange the token, resolve the tenant,<br/>tenant gate, roles and attributes
FK->>R: RequestContext filled
R->>R: read the body, resolve response, take over files
R->>S: updateObject(DTO)
S->>DB: is row 42 visible to this person? load the object
Note over S,DB: first database access: the transaction starts here
S->>S: recursion over object and children:<br/>check the role per object, transfer values,<br/>remember rule violations, queue hooks
S->>H: before hooks
H-->>S: may still change fields
S->>S: validation: re-check the remembered violations
S->>DB: hand the change to the database
S->>H: after hooks
S->>DB: flush: the SQL runs, database rules apply,<br/>Envers writes the revision
S->>DB: read back with the response (READ hook per object)
S-->>R: DTO
R->>DB: COMMIT
R-->>F: 200 with data and meta
F-->>U: the form shows the saved state
Up to and including “RequestContext filled” this is the same path as a read. From the REST layer on, it becomes a different one.
Who checks when
The order is not a matter of taste: every stage assumes the previous one passed.
-
Filter chainTokenIs the signature valid and the token not expired?↳ no 401
-
CIASTenantWhich tenant, and is it served?↳ no 403 with
cias.authentication.… -
CDMSField selectionIs there a
responsein the body, and can the body be read?↳ no 400 -
CDMSVisibilityIs the row visible to this person at all — with the same filters as on a read?↳ no 404, as if it did not exist
-
CDMSWrite roleDoes one of the effective roles allow changing
customer? Likewise for every child written along↳ no 403missing-permission|<role> -
CDMSTransfer the valuesWrite the fields from
datainto the object, set defaults, create or link children. Rule violations are only remembered↳ no 400 / 404 for children that cannot be resolved -
HookBefore hooksThe project's own business logic sees the fully populated object and may still change it↳ no The hook can refuse on its own, with its own response
-
CDMSValidationEvery remembered violation is checked against the value that stands on the object now↳ no 422
validation-failedwith all violations at once -
DatabaseSave and flushThe SQL runs, the database checks its own rules, for example unique values↳ no 409
already-existsfor a value already taken · 400constraint-violationif a value does not fit the column · 503 if the database cannot be reached -
CDMSRead backThe object is read with the
responseand with the read permissions of the person↳ no 403 / 404 – and the change is taken back with it -
CDMSCommitCommit the transaction before the response is written↳ no rollback; 409 on a detected concurrent change, otherwise a server error
- 200 with the saved object – the state that is now in the database
Why this order
| Order | Why it has to be this way |
|---|---|
| Tenant before role | Which roles apply depends on the tenant. Roles in the active tenant replace the global ones, see Effective roles. |
| Visibility before everything else | Otherwise an error message would reveal that a foreign row exists. Knowing an id is not enough. |
| Role before hooks | If a role is missing, not a single hook runs. So a hook cannot trigger anything the person was not allowed to do. |
| Hooks before validation | A before hook may fill a required field the client does not know about — a running number, for instance. See The order within a write operation. |
| Read back before the commit | The response should show the saved state. If reading fails, writing is void as well. |
| Commit before the response | Otherwise the client would get a “200” for something that can still fail afterwards. |
What a refusal leaves behind, stage by stage
Not every refusal leaves the same state. What matters is whether the request had already reached the database.
| Refused at stage | Who answers | What is in the database | Did hooks run? |
|---|---|---|---|
| token, tenant | filter chain (CIAS) | nothing, there was no transaction | no |
| field selection, body | REST layer | nothing | no |
| visibility, role | system layer | nothing; the transaction was opened for reading at most | no |
| transfer the values | system layer | nothing | no |
| a before hook refuses | hook | nothing in the database — outside it, whatever the hook triggered stays | yes, up to that hook |
| validation | system layer | nothing | yes, all before hooks |
| flush (database rule) | persistence | nothing, everything is rolled back | yes, before and after |
| read back | system layer | nothing, the change is taken back | yes, all |
| commit | persistence | nothing | yes, all |
Two sentences about this that are often missing in practice:
- From validation downwards, your before hooks have already run. Everything a hook does outside the database — a mail, a call into another system — stays, even when the request ends in an error afterwards.
- After hooks do not yet see a secured state. After them come flush, read back and commit. Each of those three can still bring the whole request down.
| All stages passed | Commit succeeds | Result |
|---|---|---|
| yes | yes | 200 with the object, the change is durable |
| yes | no | rollback; 409 on a detected concurrent change, otherwise a server error |
| no | – | error response, everything rolled back — including the parts already written before |
The transaction bracket
A transaction is the bracket around the changes: either all of them or none. In CDMS that bracket is exactly one request.
-
1CDMS→Databasestart: at the first database access of the request. On a change that is the visibility check, on a create the first saveNot already when the request arrives. A request that fails at token, tenant or field selection never opened a transaction.
-
2CDMS→Databaseflush: the collected SQL goes to the database. It checks its rules; other requests do not see any of it yet
-
3CDMS→Databasecommit: immediately before the response body is writtenResult: When the success response arrives at the client, a request right after it already sees the change.
If anything fails, the whole request is marked as failed: every open transaction is rolled back at once, and nothing of this request is committed at the end. That also covers databases that were touched earlier.
- the object itself
- all nested children created, changed or deleted
- what hooks write through CDMS on models of the same level
- the revision of the audit
- reading back for the response
- file contents in the file storage
- mails, HTTP calls and messages from a hook
- everything that concerns a second database
- everything from an earlier request
A request that writes a system model and a tenant model has two transactions in two databases. They are committed one after the other, not together. See No atomicity across two databases and One request, one transaction.
Audit and revision
If the model is audited, a revision is created on a write: a copy of the object after the change, together with who made it, when and from where.
-
1Filter chainreads IP address and user agent from the request and user id and name from the token, and puts them into the RequestContext
-
2CDMS→Databaseat the flush, Envers writes a row into
revinfoand, for every changed object, a row into its_AUDtableThe four values come from the RequestContext, not from the object. Without a request context — in a background job, for instance — they stay empty. -
3CDMS→Databasewith the commit the revision becomes durable, together with the changeResult: If the request fails, the revision vanishes with everything else. There is no revision without a change.
All changes of one request get the same revision number — one per database. What it holds and how to read it: What a revision records and Reading the history.
CIAS keeps a separate trail of its own, for role grants, suspensions and tenant changes. The two trails have nothing to do with each other, see CIAS audit and CDMS history.
The three kinds of write compared
The path is the same for all three. What differs is what it checks along the way.
When: POST /create – a new object, without an id.
No visibility check, because there is nothing to see yet. What is checked is the create role, and for every child created along with it. Validation looks at every field of the model; a missing field counts as empty. Default values are already in place at that point.
Result: 200 with the new object and its id, and a revision of type ADD in the audit. See Creating an object.
When: PUT /update/{id} or PATCH /update/{id}.
Visibility first, then the stored object is loaded, then the update role. PUT describes the whole target state and therefore checks every field; PATCH names only the change and checks only the sent fields. A child without an id inside a PATCH is created — and checked with the rules for creating.
Result: 200 with the new state, and a revision of type MOD in the audit. See PUT or PATCH? The null trap.
When: DELETE /delete/{id}.
Visibility, then the delete role per object, then dependent children are queued for removal and other relations are detached. There is no validation, no reading back and no READ hook — nothing comes back, after all.
Result: 200 without a body, and a revision of type DEL holding only the id. See How a DELETE runs.
And in the standalone operating mode?
Exactly the same. The difference between “CIAS embedded” and “CIAS as its own service” lies entirely before the REST layer: the tenant gate and the attribute lookup ask their question once through a method call and once over HTTP. From the RequestContext on, the write path is the same code with the same checks.
The two ways side by side are on From login to the data, all differences on Embedded and standalone compared.
Traps
Where to go next
- From login to the data: the same path on a read
- The path of a request through the layers: the stations inside CDMS
- The order within a write operation: where exactly the hooks sit
- Validation and When a hook fails
- One request, one transaction
- What is audited