// Core Audit Types
org.springframework.boot.actuate.audit.AuditEvent
org.springframework.boot.actuate.audit.AuditEventRepository
org.springframework.boot.actuate.audit.InMemoryAuditEventRepository
org.springframework.boot.actuate.audit.AuditEventsEndpoint
// Spring Event Integration
org.springframework.boot.actuate.audit.listener.AuditApplicationEvent
org.springframework.boot.actuate.audit.listener.AbstractAuditListener
org.springframework.boot.actuate.audit.listener.AuditListener
// Security Integration
org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener
org.springframework.boot.actuate.security.AuthenticationAuditListener
org.springframework.boot.actuate.security.AbstractAuthorizationAuditListener
org.springframework.boot.actuate.security.AuthorizationAuditListenerWhat needs auditing?
│
├─ Security events (login, logout, access denied)?
│ ├─ Authentication events → AuthenticationAuditListener
│ └─ Authorization events → AuthorizationAuditListener
│
├─ Business events (data changes, operations)?
│ └─ Custom AuditApplicationEvent + AuditListener
│
├─ Compliance tracking?
│ └─ Persistent AuditEventRepository (database)
│
└─ Temporary monitoring?
└─ InMemoryAuditEventRepository (capacity-limited)| Repository Type | Capacity | Persistence | Use When |
|---|---|---|---|
| InMemoryAuditEventRepository | Limited (configurable) | No | Development, testing |
| Custom JDBC/JPA Repository | Unlimited | Yes | Production, compliance |
| Custom External Repository | Unlimited | Yes | Centralized logging |
// Authentication
AuthenticationAuditListener.AUTHENTICATION_SUCCESS
AuthenticationAuditListener.AUTHENTICATION_FAILURE
AuthenticationAuditListener.AUTHENTICATION_SWITCH
AuthenticationAuditListener.LOGOUT_SUCCESS
// Authorization
AuthorizationAuditListener.AUTHORIZATION_FAILURE
// Custom
"DATA_EXPORT", "CONFIG_CHANGE", "ADMIN_ACTION", "BULK_DELETE"Always audit:
Consider auditing:
Don't audit:
PATTERN: Use built-in security listeners
// ✓ Automatic security event auditing
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}
@Bean
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}ANTI-PATTERN: Manual security event tracking
// ❌ Reinventing the wheel
@Component
public class ManualSecurityAuditor {
@EventListener
public void onLogin(AuthenticationSuccessEvent event) {
// Manual audit event creation
// Already handled by AuthenticationAuditListener
}
}PATTERN: Structured event data
// ✓ Structured, queryable data
AuditEvent event = new AuditEvent(
principal,
"DATA_EXPORT",
Map.of(
"recordCount", 1000,
"format", "CSV",
"dataType", "users"
)
);ANTI-PATTERN: Unstructured event data
// ❌ Hard to query
AuditEvent event = new AuditEvent(
principal,
"DATA_EXPORT",
"exported 1000 users as CSV" // String instead of Map
);PATTERN: Use Spring events for decoupling
// ✓ Decoupled - service doesn't know about audit
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
public void deleteUser(String userId) {
// Business logic
userRepository.delete(userId);
// Publish event
eventPublisher.publishEvent(new AuditApplicationEvent(
currentUser(),
"USER_DELETE",
Map.of("userId", userId)
));
}
}ANTI-PATTERN: Direct audit repository coupling
// ❌ Tightly coupled
@Service
public class UserService {
private final AuditEventRepository auditRepository;
public void deleteUser(String userId) {
userRepository.delete(userId);
// Direct coupling to audit infrastructure
auditRepository.add(new AuditEvent(...));
}
}The audit framework provides flexible event tracking and storage for recording security events, authentication attempts, authorization decisions, and custom application events. It includes a repository abstraction for persistence and Spring event integration for decoupled event publishing.
/**
* Value object representing an audit event.
*
* Thread-safe: Immutable after construction
* Nullability: Principal can be null, data values can be null
* Serializable: Yes (for storage/transmission)
* Since: Spring Boot 1.0+
*/
public class AuditEvent implements Serializable {
/**
* Create audit event for current time.
*
* @param principal User principal (nullable for anonymous)
* @param type Event type (non-null)
* @param data Event data map (values can be null)
*/
public AuditEvent(@Nullable String principal, String type, Map<String, @Nullable Object> data);
/**
* Create audit event from name-value pairs.
*
* @param principal User principal (nullable)
* @param type Event type (non-null)
* @param data Event data as "key=value" or "key" strings
*/
public AuditEvent(@Nullable String principal, String type, String... data);
/**
* Create audit event with specific timestamp.
*
* @param timestamp Event timestamp (non-null)
* @param principal User principal (nullable)
* @param type Event type (non-null)
* @param data Event data (values can be null)
*/
public AuditEvent(Instant timestamp, @Nullable String principal, String type,
Map<String, @Nullable Object> data);
public Instant getTimestamp();
/**
* Returns the user principal responsible for the event or an empty string
* if the principal is not available.
* NOTE: Never returns null - returns "" (empty string) when principal is unavailable.
*/
public String getPrincipal();
public String getType();
public Map<String, @Nullable Object> getData();
}
/**
* Repository for storing and retrieving audit events.
*
* Thread-safe: Implementations must be thread-safe
* Since: Spring Boot 1.0+
*/
public interface AuditEventRepository {
/**
* Log an audit event.
*
* @param event Event to store (non-null)
*/
void add(AuditEvent event);
/**
* Find audit events matching criteria.
*
* @param principal Principal filter (null = all principals)
* @param after Time filter (null = all times)
* @param type Event type filter (null = all types)
* @return List of matching events (never null, may be empty)
*/
List<AuditEvent> find(@Nullable String principal, @Nullable Instant after, @Nullable String type);
}
/**
* In-memory audit event repository.
*
* Thread-safe: Yes (synchronized internally)
* Capacity: Configurable, default 1000
* Eviction: FIFO when capacity reached
* Since: Spring Boot 1.0+
*/
public class InMemoryAuditEventRepository implements AuditEventRepository {
/**
* Create with default capacity (1000).
*/
public InMemoryAuditEventRepository();
/**
* Create with custom capacity.
*
* @param capacity Maximum events to store
*/
public InMemoryAuditEventRepository(int capacity);
/**
* Set capacity (removes oldest if reduced).
*
* @param capacity New capacity
*/
public void setCapacity(int capacity);
@Override
public void add(AuditEvent event);
@Override
public List<AuditEvent> find(@Nullable String principal, @Nullable Instant after, @Nullable String type);
}
/**
* Actuator endpoint that exposes audit events.
* Provides read-only access to audit event repository via HTTP/JMX.
*
* Thread-safe: Yes (delegates to repository)
* Nullability: All parameters nullable
* Since: Spring Boot 2.0.0
*/
@Endpoint(id = "auditevents")
public class AuditEventsEndpoint {
/**
* Create endpoint with audit event repository.
*
* @param auditEventRepository Repository to query (non-null)
*/
public AuditEventsEndpoint(AuditEventRepository auditEventRepository);
/**
* Retrieve audit events matching criteria.
* All parameters are optional filters.
*
* @param principal Filter by principal name (nullable)
* @param after Filter by events after this time (nullable)
* @param type Filter by event type (nullable)
* @return Descriptor containing matching events (never null)
*/
@ReadOperation
public AuditEventsDescriptor events(@Nullable String principal,
@Nullable OffsetDateTime after,
@Nullable String type);
/**
* Response body for audit events endpoint.
* Wraps list of audit events for serialization.
*/
public static final class AuditEventsDescriptor implements OperationResponseBody {
/**
* Get the list of audit events.
*
* @return List of audit events (never null, may be empty)
*/
public List<AuditEvent> getEvents();
}
}
/**
* Spring ApplicationEvent wrapper for AuditEvent.
* Allows audit events to be published through Spring's event system.
*
* Thread-safe: Yes (immutable after construction)
* Since: Spring Boot 1.0+
*/
public class AuditApplicationEvent extends ApplicationEvent {
private final AuditEvent auditEvent;
/**
* Create a new {@link AuditApplicationEvent} that wraps a newly created
* {@link AuditEvent}.
*
* @param principal the principal (nullable for anonymous)
* @param type the event type (non-null)
* @param data the event data (non-null, values can be null)
* @see AuditEvent#AuditEvent(String, String, Map)
*/
public AuditApplicationEvent(String principal, String type,
Map<String, @Nullable Object> data);
/**
* Create a new {@link AuditApplicationEvent} that wraps a newly created
* {@link AuditEvent}.
*
* @param principal the principal (nullable)
* @param type the event type (non-null)
* @param data the event data (non-null)
* @see AuditEvent#AuditEvent(String, String, String...)
*/
public AuditApplicationEvent(String principal, String type, String... data);
/**
* Create a new {@link AuditApplicationEvent} that wraps a newly created
* {@link AuditEvent}.
*
* @param timestamp the timestamp (non-null)
* @param principal the principal (nullable)
* @param type the event type (non-null)
* @param data the event data (non-null, values can be null)
* @see AuditEvent#AuditEvent(Instant, String, String, Map)
*/
public AuditApplicationEvent(Instant timestamp, String principal, String type,
Map<String, @Nullable Object> data);
/**
* Create a new {@link AuditApplicationEvent} that wraps the specified
* {@link AuditEvent}.
*
* @param auditEvent the source of this event (non-null)
*/
public AuditApplicationEvent(AuditEvent auditEvent);
/**
* Get the audit event.
*
* @return the audit event (never null)
*/
public AuditEvent getAuditEvent();
}
/**
* Base class for audit event listeners.
* Provides infrastructure for listening to audit events and storing them.
*
* Thread-safe: Yes
* Since: Spring Boot 1.0+
*/
public abstract class AbstractAuditListener implements ApplicationListener<AuditApplicationEvent> {
/**
* Called when an audit application event is received.
* Subclasses can override to perform additional processing.
*
* @param event the audit event (non-null)
*/
protected abstract void onAuditEvent(AuditEvent event);
@Override
public void onApplicationEvent(AuditApplicationEvent event) {
onAuditEvent(event.getAuditEvent());
}
}
/**
* Default audit listener that stores events in a repository.
* Listens for AuditApplicationEvents and stores them in an AuditEventRepository.
*
* Thread-safe: Yes
* Auto-configured: No (manual registration required)
* Since: Spring Boot 1.0+
*/
public class AuditListener extends AbstractAuditListener {
/**
* Create audit listener with repository.
*
* @param auditEventRepository Repository for storing events (non-null)
*/
public AuditListener(AuditEventRepository auditEventRepository);
/**
* Store audit event in repository.
*
* @param event Audit event to store (non-null)
*/
@Override
protected void onAuditEvent(AuditEvent event);
}package com.example.actuator;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository;
import org.springframework.boot.actuate.audit.listener.AuditListener;
import org.springframework.boot.actuate.security.AuthenticationAuditListener;
import org.springframework.boot.actuate.security.AuthorizationAuditListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration for audit framework with security event tracking.
*
* Automatically captures:
* - Authentication success/failure
* - Authorization failures
* - Logout events
* - User switching
*
* Thread-safe: Yes
* Since: Application 1.0
*/
@Configuration
public class AuditConfiguration {
/**
* In-memory audit event repository.
* For production, replace with persistent implementation.
*
* @return Audit repository with 5000 event capacity
*/
@Bean
public AuditEventRepository auditEventRepository() {
InMemoryAuditEventRepository repository =
new InMemoryAuditEventRepository(5000);
return repository;
}
/**
* Audit listener that stores events from ApplicationEventPublisher.
*
* @param repository Repository for storage
* @return Configured audit listener
*/
@Bean
public AuditListener auditListener(AuditEventRepository repository) {
return new AuditListener(repository);
}
/**
* Authentication audit listener for Spring Security.
* Captures: login, logout, authentication failures.
*
* @return Authentication audit listener
*/
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}
/**
* Authorization audit listener for Spring Security.
* Captures: access denied events.
*
* @return Authorization audit listener
*/
@Bean
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}
}package com.example.actuator;
import org.springframework.boot.actuate.audit.AuditApplicationEvent;
import org.springframework.boot.actuate.audit.listener.AbstractAuditListener;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* Service demonstrating business event auditing.
*
* Thread-safe: Yes
* Event publishing: Asynchronous via Spring events
* Since: Application 1.0
*/
@Service
public class UserManagementService {
private static final Logger logger = LoggerFactory.getLogger(UserManagementService.class);
private final ApplicationEventPublisher eventPublisher;
public UserManagementService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* Delete user with audit trail.
*
* @param userId User ID to delete
* @param deletedBy Principal performing deletion
*/
public void deleteUser(String userId, String deletedBy) {
// Business logic
logger.info("Deleting user: {}", userId);
// Publish audit event
eventPublisher.publishEvent(new AuditApplicationEvent(
deletedBy,
"USER_DELETE",
Map.of(
"userId", userId,
"action", "delete",
"reason", "account_closure"
)
));
}
/**
* Export user data with audit trail.
*
* @param exportedBy Principal performing export
* @param format Export format
* @param recordCount Number of records
*/
public void exportUserData(String exportedBy, String format, int recordCount) {
// Business logic
logger.info("Exporting {} users as {}", recordCount, format);
// Publish audit event
eventPublisher.publishEvent(new AuditApplicationEvent(
exportedBy,
"DATA_EXPORT",
Map.of(
"dataType", "users",
"format", format,
"recordCount", recordCount
)
));
}
/**
* Change user role with audit trail.
*
* @param userId User ID
* @param oldRole Previous role
* @param newRole New role
* @param changedBy Principal making change
*/
public void changeUserRole(String userId, String oldRole, String newRole, String changedBy) {
// Business logic
logger.info("Changing role for user {}: {} -> {}", userId, oldRole, newRole);
// Publish audit event
eventPublisher.publishEvent(new AuditApplicationEvent(
changedBy,
"ROLE_CHANGE",
Map.of(
"userId", userId,
"oldRole", oldRole,
"newRole", newRole
)
));
}
}
/**
* Custom audit listener for business events.
* Performs additional logging and alerting.
*
* Thread-safe: Yes
* Since: Application 1.0
*/
@Component
public class BusinessAuditListener extends AbstractAuditListener {
private static final Logger logger = LoggerFactory.getLogger(BusinessAuditListener.class);
@Override
protected void onAuditEvent(AuditEvent event) {
// Log all audit events
logger.info("Audit Event: type={}, principal={}, data={}",
event.getType(), event.getPrincipal(), event.getData());
// Alert on sensitive operations
if ("USER_DELETE".equals(event.getType()) ||
"DATA_EXPORT".equals(event.getType())) {
alertSecurityTeam(event);
}
// Check for suspicious patterns
if (isHighRiskOperation(event)) {
logger.warn("High-risk operation detected: {}", event);
}
}
private void alertSecurityTeam(AuditEvent event) {
// Send alert to security monitoring system
logger.warn("SECURITY ALERT: {} by {}", event.getType(), event.getPrincipal());
}
private boolean isHighRiskOperation(AuditEvent event) {
// Check for patterns indicating risk
return "ROLE_CHANGE".equals(event.getType()) &&
"ADMIN".equals(event.getData().get("newRole"));
}
}package com.example.actuator;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.jspecify.annotations.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* JDBC-based persistent audit event repository.
*
* Thread-safe: Yes (via JdbcTemplate)
* Capacity: Unlimited
* Persistence: Database
* Since: Application 1.0
*
* Database schema:
* CREATE TABLE audit_events (
* id BIGINT PRIMARY KEY AUTO_INCREMENT,
* timestamp TIMESTAMP NOT NULL,
* principal VARCHAR(255),
* type VARCHAR(100) NOT NULL,
* data TEXT,
* INDEX idx_principal (principal),
* INDEX idx_timestamp (timestamp),
* INDEX idx_type (type)
* );
*/
@Repository
public class JdbcAuditEventRepository implements AuditEventRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcAuditEventRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void add(AuditEvent event) {
String sql = "INSERT INTO audit_events (timestamp, principal, type, data) VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql,
Timestamp.from(event.getTimestamp()),
event.getPrincipal(),
event.getType(),
serializeData(event.getData())
);
}
@Override
public List<AuditEvent> find(@Nullable String principal,
@Nullable Instant after,
@Nullable String type) {
StringBuilder sql = new StringBuilder("SELECT * FROM audit_events WHERE 1=1");
List<Object> params = new java.util.ArrayList<>();
if (principal != null) {
sql.append(" AND principal = ?");
params.add(principal);
}
if (after != null) {
sql.append(" AND timestamp > ?");
params.add(Timestamp.from(after));
}
if (type != null) {
sql.append(" AND type = ?");
params.add(type);
}
sql.append(" ORDER BY timestamp DESC LIMIT 1000");
return jdbcTemplate.query(sql.toString(), params.toArray(), new AuditEventRowMapper());
}
private String serializeData(Map<String, Object> data) {
// Serialize to JSON or other format
return data.toString(); // Simplified - use Jackson in real implementation
}
private Map<String, Object> deserializeData(String data) {
// Deserialize from JSON or other format
return new HashMap<>(); // Simplified - use Jackson in real implementation
}
private class AuditEventRowMapper implements RowMapper<AuditEvent> {
@Override
public AuditEvent mapRow(ResultSet rs, int rowNum) throws SQLException {
return new AuditEvent(
rs.getTimestamp("timestamp").toInstant(),
rs.getString("principal"),
rs.getString("type"),
deserializeData(rs.getString("data"))
);
}
}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.audit.AuditEvent;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class AuditEventTest {
@Test
void createEvent_WithCurrentTime_SetsTimestamp() {
Instant before = Instant.now();
AuditEvent event = new AuditEvent(
"testuser",
"TEST_EVENT",
Map.of("key", "value")
);
Instant after = Instant.now();
assertThat(event.getTimestamp()).isBetween(before, after);
assertThat(event.getPrincipal()).isEqualTo("testuser");
assertThat(event.getType()).isEqualTo("TEST_EVENT");
assertThat(event.getData()).containsEntry("key", "value");
}
@Test
void createEvent_WithNameValuePairs_ParsesData() {
AuditEvent event = new AuditEvent(
"testuser",
"TEST_EVENT",
"key1=value1", "key2=value2", "key3"
);
assertThat(event.getData()).containsKeys("key1", "key2", "key3");
assertThat(event.getData().get("key1")).isEqualTo("value1");
assertThat(event.getData().get("key2")).isEqualTo("value2");
}
@Test
void createEvent_WithNullPrincipal_ReturnsEmptyString() {
AuditEvent event = new AuditEvent(
null,
"TEST_EVENT",
Map.of()
);
assertThat(event.getPrincipal()).isEmpty();
}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository;
import java.time.Instant;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class InMemoryAuditEventRepositoryTest {
private InMemoryAuditEventRepository repository;
@BeforeEach
void setUp() {
repository = new InMemoryAuditEventRepository(100);
}
@Test
void add_StoresEvent() {
AuditEvent event = new AuditEvent("user", "TEST", Map.of());
repository.add(event);
List<AuditEvent> found = repository.find("user", null, null);
assertThat(found).hasSize(1);
assertThat(found.get(0).getPrincipal()).isEqualTo("user");
}
@Test
void find_ByPrincipal_FiltersCorrectly() {
repository.add(new AuditEvent("user1", "TEST", Map.of()));
repository.add(new AuditEvent("user2", "TEST", Map.of()));
List<AuditEvent> found = repository.find("user1", null, null);
assertThat(found).hasSize(1);
assertThat(found.get(0).getPrincipal()).isEqualTo("user1");
}
@Test
void find_ByTime_FiltersCorrectly() {
Instant now = Instant.now();
Instant past = now.minus(Duration.ofHours(1));
repository.add(new AuditEvent(past, "user", "OLD_EVENT", Map.of()));
repository.add(new AuditEvent(now, "user", "NEW_EVENT", Map.of()));
List<AuditEvent> found = repository.find(null, now.minus(Duration.ofMinutes(30)), null);
assertThat(found).hasSize(1);
assertThat(found.get(0).getType()).isEqualTo("NEW_EVENT");
}
@Test
void capacity_EvictsOldestEvents() {
repository.setCapacity(2);
repository.add(new AuditEvent("user", "EVENT1", Map.of()));
repository.add(new AuditEvent("user", "EVENT2", Map.of()));
repository.add(new AuditEvent("user", "EVENT3", Map.of()));
List<AuditEvent> all = repository.find(null, null, null);
assertThat(all).hasSize(2);
assertThat(all).extracting(AuditEvent::getType)
.containsExactlyInAnyOrder("EVENT2", "EVENT3");
}
}Problem: No audit events appearing in repository
Causes:
Solutions:
// Solution 1: Register AuditListener
@Bean
public AuditListener auditListener(AuditEventRepository repository) {
return new AuditListener(repository);
}
// Solution 2: Use AuditApplicationEvent
eventPublisher.publishEvent(new AuditApplicationEvent(
principal,
"EVENT_TYPE",
Map.of("key", "value")
));
// Solution 3: Configure repository
@Bean
public AuditEventRepository auditEventRepository() {
return new InMemoryAuditEventRepository();
}Problem: Login/logout events not audited
Causes:
Solutions:
// Register both listeners
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}
@Bean
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}
// Ensure Spring Security publishes events (default behavior)Problem: Too many events in memory
Causes:
Solutions:
// Solution 1: Reduce capacity
InMemoryAuditEventRepository repository =
new InMemoryAuditEventRepository(1000); // Lower capacity
// Solution 2: Use persistent repository
@Bean
public AuditEventRepository auditEventRepository(JdbcTemplate jdbc) {
return new JdbcAuditEventRepository(jdbc);
}
// Solution 3: Add cleanup job
@Scheduled(fixedRate = 3600000) // Every hour
public void cleanupOldEvents() {
jdbcTemplate.update(
"DELETE FROM audit_events WHERE timestamp < ?",
Timestamp.from(Instant.now().minus(Duration.ofDays(30)))
);
}// Calculate capacity based on event rate
// Example: 100 events/hour * 24 hours = 2400 events/day
@Bean
public InMemoryAuditEventRepository auditEventRepository() {
// Store 7 days of events
int eventsPerDay = 2400;
int days = 7;
int capacity = eventsPerDay * days;
return new InMemoryAuditEventRepository(capacity);
}// Audit event publishing is asynchronous
// No performance impact on business operations
@Service
public class PerformantService {
private final ApplicationEventPublisher eventPublisher;
public void criticalOperation() {
// Business logic (synchronous)
performCriticalWork();
// Audit (asynchronous via Spring events)
eventPublisher.publishEvent(new AuditApplicationEvent(...));
// Returns immediately, event processed async
}
}// For large audit tables, ensure proper indexing
/*
CREATE INDEX idx_audit_principal ON audit_events(principal);
CREATE INDEX idx_audit_timestamp ON audit_events(timestamp);
CREATE INDEX idx_audit_type ON audit_events(type);
CREATE INDEX idx_audit_composite ON audit_events(principal, timestamp, type);
*/
// Use pagination for large result sets
public List<AuditEvent> findPaginated(String principal, Instant after, int page, int size) {
String sql = "SELECT * FROM audit_events WHERE principal = ? AND timestamp > ? " +
"ORDER BY timestamp DESC LIMIT ? OFFSET ?";
return jdbcTemplate.query(sql, rowMapper, principal, after, size, page * size);
}