⬅️ Back to Layers Overview

Audit Logging

Pattern Overview

Changes are tracked through createdBy, createdAt, updatedBy, and updatedAt fields set via Spring Security context.

Core Implementation

Audit fields are immutable fields populated from Spring Security:

@Entity
@Table(name = "SUPPLIER")
@Data
@NoArgsConstructor
public class Supplier {
    
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @Column(name = "NAME", nullable = false, unique = true)
    private String name;
    
    // Audit fields
    @Column(name = "CREATED_BY", nullable = false, updatable = false)
    private String createdBy;
    
    @Column(name = "CREATED_AT", nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @Column(name = "UPDATED_BY")
    private String updatedBy;
    
    @Column(name = "UPDATED_AT")
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

Setting Audit Fields in Service

Services set audit fields from the current authenticated user:

@Service
@RequiredArgsConstructor
public class SupplierServiceImpl implements SupplierService {
    
    private final SupplierRepository repository;
    private final SupplierMapper mapper;
    
    @Transactional
    public SupplierDTO create(CreateSupplierDTO dto) {
        Supplier supplier = mapper.toEntity(dto);
        
        // Get current user from SecurityContext
        String currentUser = getCurrentUsername();
        
        // Set audit fields
        supplier.setCreatedBy(currentUser);
        supplier.setCreatedAt(LocalDateTime.now());
        
        return mapper.toDTO(repository.save(supplier));
    }
    
    @Transactional
    public SupplierDTO update(String id, UpdateSupplierDTO dto) {
        Supplier supplier = repository.findById(id)
            .orElseThrow(() -> new NoSuchElementException("Not found"));
        
        // Update business fields
        supplier.setName(dto.getName());
        supplier.setContactName(dto.getContactName());
        
        // Update audit fields
        supplier.setUpdatedBy(getCurrentUsername());
        supplier.setUpdatedAt(LocalDateTime.now());
        
        return mapper.toDTO(repository.save(supplier));
    }
    
    private String getCurrentUsername() {
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        return auth != null ? auth.getName() : "SYSTEM";
    }
}

Automatic Timestamp Management

Using Spring’s @CreationTimestamp and @UpdateTimestamp (Hibernate):

@Entity
@Table(name = "INVENTORY_ITEM")
@Data
public class InventoryItem {
    
    @Id
    private String id;
    
    private String name;
    
    // Automatically set on creation
    @Column(updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    // Automatically updated on modification
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

Note: Timestamps are automatic, but user tracking (createdBy, updatedBy) must be set manually in service.

Query Example: Filter by User

Find all items created by a specific user:

@Repository
public interface InventoryItemRepository extends JpaRepository<InventoryItem, String> {
    
    List<InventoryItem> findByCreatedBy(String username);
    
    List<InventoryItem> findByCreatedByAndCreatedAtAfter(
        String username, 
        LocalDateTime date);
}

@Service
@RequiredArgsConstructor
public class InventoryItemServiceImpl implements InventoryItemService {
    
    private final InventoryItemRepository repository;
    
    public List<InventoryItemDTO> findByCurrentUser() {
        String currentUser = getCurrentUsername();
        return repository.findByCreatedBy(currentUser)
            .stream()
            .map(mapper::toDTO)
            .collect(toList());
    }
}

Audit Trail Example

View complete audit trail for an item:

// Entity with audit fields
InventoryItem item = repository.findById(itemId).get();

// Access complete audit information
System.out.println("Created by: " + item.getCreatedBy());
System.out.println("Created at: " + item.getCreatedAt());
System.out.println("Updated by: " + item.getUpdatedBy());
System.out.println("Updated at: " + item.getUpdatedAt());

// Output example:
// Created by: john.doe
// Created at: 2025-01-15 10:30:00
// Updated by: jane.smith
// Updated at: 2025-01-16 14:22:15

StockHistory: Immutable Audit Trail

For truly immutable audit trails, use create-only entities:

@Entity
@Table(name = "STOCK_HISTORY")
@Data
@NoArgsConstructor
public class StockHistory {
    
    @Id
    private String id;
    
    @ManyToOne
    @JoinColumn(name = "ITEM_ID", nullable = false)
    private InventoryItem item;
    
    @Column(name = "OLD_QUANTITY")
    private Integer oldQuantity;
    
    @Column(name = "NEW_QUANTITY")
    private Integer newQuantity;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "REASON")
    private StockChangeReason reason;
    
    // Audit fields (immutable)
    @Column(name = "CREATED_BY", nullable = false, updatable = false)
    private String createdBy;
    
    @Column(name = "CREATED_AT", nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;
}

// Service creates entries, never updates
@Service
@RequiredArgsConstructor
public class StockHistoryServiceImpl implements StockHistoryService {
    
    private final StockHistoryRepository repository;
    
    @Transactional
    public StockHistoryDTO create(StockHistoryCreateDTO dto) {
        StockHistory history = new StockHistory();
        history.setItem(dto.getItem());
        history.setOldQuantity(dto.getOldQuantity());
        history.setNewQuantity(dto.getNewQuantity());
        history.setReason(dto.getReason());
        history.setCreatedBy(getCurrentUsername());
        
        return mapper.toDTO(repository.save(history));
    }
    
    // No update method - entries are immutable
    
    private String getCurrentUsername() {
        return SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();
    }
}

Anti-Pattern: Missing Audit Fields

// ❌ Bad - No audit trail
@Entity
public class Supplier {
    private String id;
    private String name;
    // No createdBy, createdAt, updatedBy, updatedAt
}

@Service
public class SupplierServiceImpl {
    public SupplierDTO create(CreateSupplierDTO dto) {
        return mapper.toDTO(repository.save(mapper.toEntity(dto)));
    }
}

Best Practice: Complete Audit Trail

// ✅ Good - Complete audit trail
@Entity
public class Supplier {
    private String id;
    private String name;
    
    @Column(updatable = false)
    private String createdBy;
    
    @Column(updatable = false)
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    private String updatedBy;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Service
@RequiredArgsConstructor
public class SupplierServiceImpl {
    
    @Transactional
    public SupplierDTO create(CreateSupplierDTO dto) {
        Supplier supplier = mapper.toEntity(dto);
        supplier.setCreatedBy(getCurrentUsername());
        supplier.setCreatedAt(LocalDateTime.now());
        
        return mapper.toDTO(repository.save(supplier));
    }
    
    private String getCurrentUsername() {
        return SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();
    }
}

⬅️ Back to Layers Overview