skip to content

How does a data-access layer fill created and updated timestamps and actor columns through lifecycle hooks?

level: middleimportance: should knowfreq 54%

answer

  1. callbacks around insert, update, delete
  2. runs before the statement is composed
  3. clock injected, actor ambient
  4. same statement, not a second write
  5. bulk and hand-written paths stamp nothing

basics

~20 s

Registered callbacks run around insert, update and delete and set the fields before the statement is built — the time from an injected clock, the actor from ambient context — so callers never assign audit columns.

solid answer

~50 s

Mapping layers expose lifecycle hooks — callbacks invoked before or after an object is inserted, updated, deleted, or loaded. Audit stamping registers a **before-insert** hook that sets the created timestamp, created-by and initial revision, and a **before-update** hook that sets the updated timestamp and updated-by. Because the hook runs before the statement is composed, the values it assigns are part of the same INSERT or UPDATE, not a second write. The time comes from an injected clock rather than a direct call to the system clock, so tests can pin it; the actor comes from the ambient security context bound at the boundary. The two things to be careful about are the paths that never invoke a hook — set-based statements, hand-written SQL, cascades in the database — and what a hook should do when there is no ambient actor.

go deeper

for a junior

Know that the layer can fill created and updated columns for you through callbacks it invokes around writes, so service code does not assign them by hand.

for a middle

Explain that the before-insert and before-update callbacks run before the statement is composed, so the values ride the same INSERT or UPDATE, and say where the time and the actor come from.

for a senior

Show that you have thought about the gaps: unattended writes with no principal, set-based and hand-written statements that stamp nothing, and hooks that must not do work beyond the object they receive.

for a principal

Own the policy question — whether an unattributable write is allowed at all, and whether audit columns on the row are the right mechanism versus a dedicated change record with its own lifecycle.

## What a lifecycle hook is A mapping layer that tracks objects knows exactly when each one is about to be written. It can therefore offer **callbacks** at defined points in an object's life: before and after insert, before and after update, before and after delete, and after load. The callback may live on the mapped class itself or in a separate listener registered for many types; either way the layer calls it, passing the object, at the moment named. Audit stamping is the canonical use. Instead of every service assigning `createdAt`, `createdBy`, `updatedAt`, `updatedBy` and a revision counter, one listener does it for every type that declares those fields. ## The two hooks that do the work | Hook | Fires | Typically sets | |---|---|---| | Before insert | Once, as a new object is about to be written | Created timestamp, created-by, updated timestamp, updated-by, revision start | | Before update | Each time a tracked object is found modified and flushed | Updated timestamp, updated-by, revision increment | | Before delete | As a removal is about to be written | For a marker-based removal: the deleted timestamp and deleted-by | | After load | When an object is materialised from a row | Nothing audit-related — useful for deriving transient state | Two details matter. First, **before-insert must set the updated columns too**, otherwise a freshly created row has a null `updatedAt` and every "sort by last change" query has to special-case it. Second, the hook runs **before the statement is composed**, so the values become part of the original INSERT or UPDATE. A stamp written by issuing a second UPDATE from inside a hook is a different and much worse design: it doubles the statements, can re-enter the flush, and may itself trip the update hook again. ## Where the ambient values come from A hook has no arguments beyond the object, so everything it needs must be ambient: - **Time** — from an injected clock abstraction, not a direct system-clock call. This is what lets a test assert an exact stamp and lets the whole unit of work share one instant rather than drifting by milliseconds across the objects flushed in it. - **Actor** — from the security context established when the request was authenticated, bound so that concurrent units of work each see their own. - **Tenant**, when the same listener also fills an owning-tenant column on insert, from the same boundary binding as the read-side filter uses. The awkward cases are the ones with no request behind them: a scheduled job, a data import, a consumer of a message, a one-off maintenance task. There is no human actor, and the honest options are to bind a **synthetic principal** naming the job, or to refuse the write. Writing null is the option that looks harmless and hurts later, because "who changed this" then has no answer for exactly the writes nobody watched. ## Ordering and re-entrancy When several listeners are registered for the same event, the order in which they run is a property of the layer's configuration, and a design whose correctness depends on that order is fragile — the same lesson database triggers teach. Keep each hook independent: if one listener sets the timestamp and another derives a value from it, you have coupled them through invisible ordering. Re-entrancy is the sharper trap. A hook fires **during** the flush, while the layer is walking the set of changed objects and turning them into statements. Loading another object, running a query, or registering a new object from inside that hook mutates the very collection being iterated. Layers differ in how much of this they tolerate; the portable rule is that a stamping hook should touch **only the object it was handed**. Anything that needs to write elsewhere — a history row, an outbox message — belongs to a mechanism designed for it, not to a stamping callback. ## What never reaches a hook Hooks are invoked by the object-tracking machinery. Anything that bypasses that machinery bypasses the stamps: 1. a set-based update or delete that becomes one SQL statement over many rows; 2. a hand-written statement executed through the layer; 3. rows changed by the database itself — a declared referential action, or logic living in the engine; 4. a bulk import path that writes rows directly for speed. Each of those leaves rows whose `updatedAt` is stale and whose `updatedBy` still names whoever last touched them through the normal path — which is worse than a null, because it is confidently wrong. If a code path must bypass the hooks, it has to set the columns in its own statement. ## Practical rules - Put the stamping in one listener, not copied into each mapped class. - Take time from a clock you can substitute; take the actor from the boundary; decide the no-actor policy explicitly. - Set both created and updated columns on insert. - Keep hooks confined to the object they receive, and independent of each other's ordering. - Treat set-based and hand-written writes as paths that must stamp themselves.

  • Why take the timestamp from an injected clock instead of calling the system clock inside the hook?
    Two reasons. Tests can pin the instant and assert the stamp exactly, instead of asserting a range. And one clock shared for the unit of work gives every object flushed together the same instant, so rows written in one transaction sort and group as one change rather than drifting apart by milliseconds.
  • A background job with no logged-in user writes rows. What should the actor column contain?
    A synthetic principal identifying the job — a stable name such as an importer or scheduler identity — bound the same way a request principal is. Null is the tempting option and the worst one, because unattended writes are precisely the ones you later need attributed. Refusing the write is defensible where every change must be attributable to a human.
  • Is it acceptable for a stamping hook to load another object or issue a query?
    Avoid it. The hook runs while the layer is iterating the changed objects to build statements, so loading or registering more work mutates that iteration; layers differ in how much they tolerate and the failures are order-dependent. A stamping hook should touch only the object it is given.

saying these in an interview costs you the question

  • Has the hook issue a second UPDATE to write the stamp
  • Leaves the updated timestamp null on insert
  • Reads the system clock directly inside the hook, so tests assert ranges
  • Writes a null actor for background jobs and calls it acceptable
  • Assumes set-based updates still fire the update hook
  • Relies on one listener running before another