skip to content

How does @CreatedBy / @LastModifiedBy get the current user? Explain AuditorAware and how you'd implement it with Spring Security.

level: middleimportance: must knowfreq 68%

answer

  1. AuditorAware<T>, one method, Optional<T> getCurrentAuditor()
  2. read SecurityContextHolder Authentication
  3. filter null / not-authenticated / anonymous
  4. empty() -> field stays null, no error
  5. auditorAwareRef when multiple beans

basics

~20 s

You implement AuditorAware<T>, a functional interface whose getCurrentAuditor() returns Optional of the current user. Register it as a bean; Spring calls it during persist/update to fill @CreatedBy and @LastModifiedBy. With Spring Security you read the principal from SecurityContextHolder.

solid answer

~40 s

@CreatedBy/@LastModifiedBy can't be inferred, so Spring asks a bean you provide: AuditorAware<T>, a functional interface with one method, Optional<T> getCurrentAuditor(). During @PrePersist and @PreUpdate the AuditingEntityListener calls it and writes whatever it returns into the audit fields; T is the type of those fields (String username, Long id, etc.). With Spring Security you read SecurityContextHolder.getContext().getAuthentication(), guard against null or an unauthenticated/anonymous token, and return Optional.of the username or user id. Returning Optional.empty() (e.g. for a system/background job with no security context) leaves the fields untouched — meaning null on insert. You point auditing at the bean by name via @EnableJpaAuditing(auditorAwareRef = "...") when you have more than one, otherwise Spring finds the single AuditorAware bean automatically.

code

java · 13 lines
java
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class AuditConfig {

    @Bean
    AuditorAware<String> auditorProvider() {
        return () -> Optional.ofNullable(
                    SecurityContextHolder.getContext().getAuthentication())
                .filter(Authentication::isAuthenticated)
                .filter(a -> !(a instanceof AnonymousAuthenticationToken))
                .map(Authentication::getName);
    }
}

go deeper

for a junior

Know that AuditorAware supplies the current user and is read from Spring Security.

for a middle

Implement it defensively (null/anonymous filtering), know it returns Optional, and register it as a bean.

for a senior

Explain the thread-local SecurityContext boundary for async/scheduled work and the bulk-update lifecycle bypass.

for a principal

Design a propagation/fallback strategy for system actors and reactive stacks; consider ReactiveAuditorAware for WebFlux.

## Why a callback is needed Timestamps come from the clock, but the *identity* of the current user is application-specific — it might live in Spring Security, a JWT claim, a thread-local, or a request header. So Spring defines a Strategy interface you implement: **`AuditorAware<T>`**. ```java public interface AuditorAware<T> { Optional<T> getCurrentAuditor(); } ``` It is a `@FunctionalInterface` with a single method returning `Optional<T>`. `T` must match the declared type of your `@CreatedBy`/`@LastModifiedBy` fields — if those are `String`, return `Optional<String>`; if `Long` user ids, return `Optional<Long>`. ## How it is invoked When `AuditingEntityListener` fires on `@PrePersist`/`@PreUpdate`, it resolves the configured `AuditorAware` bean and calls `getCurrentAuditor()`: - `Optional.of(value)` → the value is written to `@CreatedBy` (on insert) and/or `@LastModifiedBy` (on insert + update). - `Optional.empty()` → the field is left as-is (null for a fresh insert). No exception is thrown. ## Spring Security implementation The canonical implementation reads the current `Authentication`: ```java class SpringSecurityAuditorAware implements AuditorAware<String> { public Optional<String> getCurrentAuditor() { return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) .filter(Authentication::isAuthenticated) .filter(a -> !(a instanceof AnonymousAuthenticationToken)) .map(Authentication::getName); } } ``` Key defensive points: - `getAuthentication()` can be `null` (no security context) → wrap with `ofNullable`. - Filter out unauthenticated and `AnonymousAuthenticationToken` so "anonymous" doesn't get recorded as a real user. - `getName()` yields the username; alternatively cast the principal to your `UserDetails`/custom type to extract an id. ## Registering and selecting the bean Expose it as a `@Bean`. With exactly one `AuditorAware` bean, `@EnableJpaAuditing` finds it by type. With several, disambiguate: ```java @EnableJpaAuditing(auditorAwareRef = "auditorProvider") ``` ## The SecurityContextHolder / thread-boundary gotcha `SecurityContextHolder` is thread-local by default (`MODE_THREADLOCAL`). Auditing runs on the same thread as the persist, so within a normal request that's fine. But in `@Async` methods, `@Scheduled` jobs, reactive flows, or new threads, the security context is *not* propagated automatically — `getCurrentAuditor()` returns empty and audit-by fields go null. Solutions include `DelegatingSecurityContextExecutor`, `SecurityContextHolder.MODE_INHERITABLETHREADLOCAL`, or having system jobs return a sentinel like `"system"`. ## Edge cases - **Bulk JPQL/`@Modifying` updates** bypass the entity lifecycle entirely — no listener fires, so `@LastModifiedBy`/`@LastModifiedDate` are NOT updated. This is a frequent surprise. - **`saveAll` / merge of detached entities**: created-by is only set when the object is first persisted; re-saving an existing row updates only the modified-by fields.

  • A @Scheduled batch job persists entities but @LastModifiedBy comes out null. Why?
    The scheduler runs on its own thread with no propagated SecurityContext, so getCurrentAuditor() returns Optional.empty(). Propagate the context (DelegatingSecurityContextExecutor / inheritable thread-local) or return a fixed 'system' auditor for those jobs.
  • Does a bulk @Modifying JPQL UPDATE trigger @LastModifiedBy/@LastModifiedDate?
    No. Bulk JPQL/native updates bypass the JPA entity lifecycle, so AuditingEntityListener never fires and the audit fields are not touched. You'd have to set them explicitly in the query.

saying these in an interview costs you the question

  • Returning null instead of Optional.empty() from getCurrentAuditor()
  • Not filtering AnonymousAuthenticationToken, so 'anonymousUser' gets recorded
  • Assuming SecurityContext is available in @Async/@Scheduled threads
  • Expecting bulk JPQL updates to bump @LastModifiedBy

context