Audit logging is a crucial part of enterprise applications. Whether you’re building a banking platform, an insurance portal, or an e-commerce API, you must track who did what and when.
In this guide, we’ll build a fully functional Audit Logging system for a Spring Boot REST API. You’ll learn how to capture and persist audit logs automatically for every controller action — without manually adding log statements in each method.
What Is Audit Logging?
Audit loggingrecords what actions were performed in your application, by whom, and when.
In a REST API, audit logs are useful for:
- Tracking who created, updated, or deleted resources
- Investigating issues
- Maintaining compliance or data integrity
We’ll build this with Spring Boot, JPA, and Aspect-Oriented Programming (AOP).
Real-World Use Case
Imagine a Product Management System where multiple users create, update, and delete products through REST APIs.
For compliance and debugging, you need to record:
- Which user performed the action
- What API method was called
- Input parameters
- Timestamp of the event
Instead of manually logging in every controller, we’ll use Spring AOP (Aspect-Oriented Programming) to intercept and persist audit logs automatically.
Project Structure
Below is the structure of our project:
auditlogging
│
├── src/main/java
│ └── com.example.auditlogging
│ ├── AuditloggingApplication.java
│ ├── aspect/
│ │ └── AuditAspect.java
│ ├── config/
│ │ ├── AsyncConfig.java
│ │ └── SecurityConfig.java
│ ├── controller/
│ │ └── ProductController.java
│ ├── entity/
│ │ ├── AuditLog.java
│ │ └── Product.java
│ ├── filter/
│ │ └── CachingRequestResponseFilter.java
│ ├── repository/
│ │ ├── AuditLogRepository.java
│ │ └── ProductRepository.java
│ └── service/
│ └── AuditService.java
│
└── src/main/resources
├── application.properties
├── schema.sql
└── data.sql
Step 1: Application Entry Point
AuditloggingApplication.java
package com.example.auditlogging;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AuditloggingApplication {
public static void main(String[] args) {
SpringApplication.run(AuditloggingApplication.class, args);
}
}
Explanation
- @EnableAspectJAutoProxy enables AOP features in Spring.
- This class bootstraps the application and loads all beans.
Step 2: Entity Classes
AuditLog.java
/**
*
*/
package com.example.auditlogging.entity;
/**
*
*/
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "AUDIT_LOG")
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String action;
@Column(length = 2000)
private String details;
private String username;
private LocalDateTime timestamp;
public AuditLog() {}
public AuditLog(String action, String details, String username, LocalDateTime timestamp) {
this.action = action;
this.details = details;
this.username = username;
this.timestamp = timestamp;
}
// Getters & Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}
Explanation
- Represents the AUDIT_LOG table.
- Stores method name, parameters, user, and timestamp.
- This table will automatically capture entries whenever an API is called.
- Columns
- id: Primary key (auto-generated)
- action: The controller method name
- details: Information about the call (arguments, etc.)
- username: Name of the user who performed the action
- timestamp: When it happened
Product Entity
Product.java
/**
*
*/
package com.example.auditlogging.entity;
/**
*
*/
import jakarta.persistence.*;
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String category;
private Double price;
public Product() {}
public Product(String name, String category, Double price) {
this.name = name;
this.category = category;
this.price = price;
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
Explanation
- Represents a simple domain entity to perform CRUD operations.
- The actions performed here will generate audit logs.
Step 3: Repository Layer
AuditLogRepository.java
@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
}
ProductRepository.java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {}
Explanation
- Provides database operations for our entities.
- Spring Data JPA auto-implements CRUD methods.
Step 4: Controller Layer
ProductController.java
import com.example.auditlogging.entity.*;
import com.example.auditlogging.repository.*;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository productRepository;
public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@GetMapping
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@PostMapping
public Product createProduct(@RequestBody Product product) {
return productRepository.save(product);
}
@PutMapping("/{id}")
public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
product.setId(id);
return productRepository.save(product);
}
@DeleteMapping("/{id}")
public void deleteProduct(@PathVariable Long id) {
productRepository.deleteById(id);
}
}
Explanation
- A simple REST controller performing CRUD on Product.
- Every method is intercepted by our AuditAspect.
This controller exposes four REST endpoints:
- GET /api/products — fetch all products
- POST /api/products — create product
- PUT /api/products/{id} — update product
- DELETE /api/products/{id} — delete product
Step 5: Aspect Layer — The Core Audit Logic
AuditAspect.java
import com.example.auditlogging.entity.*;
import com.example.auditlogging.repository.*;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.util.Arrays;
@Aspect
@Component
public class AuditAspect {
private static final Logger logger = LoggerFactory.getLogger(AuditAspect.class);
@Autowired
private final AuditLogRepository auditLogRepository;
public AuditAspect(AuditLogRepository auditLogRepository) {
this.auditLogRepository = auditLogRepository;
}
// Pointcut to capture all controller methods
// @Pointcut("within(com.example.auditdemo.controller..*)")
@Pointcut("execution(* com.example.auditlogging.controller.ProductController.*(..))")
public void controllerMethods() {}
// After a successful return from any controller method
@AfterReturning(value = "controllerMethods()", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
try {
String method = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
AuditLog log = new AuditLog();
log.setAction(method.toUpperCase());
log.setDetails("Method " + method + " executed with args " + args);
log.setTimestamp(LocalDateTime.now());
log.setUsername("system");
auditLogRepository.save(log);
// System.out.println("✅ Audit log saved for " + method);
logger.info("✅ Audit log saved successfully for method: {}", method);
} catch (Exception e) {
System.err.println("❌ Error saving audit log: " + e.getMessage());
logger.error("⚠️ Failed to save audit log: {}", e.getMessage());
e.printStackTrace();
}
}
}
Explanation
- Uses AspectJ annotations to intercept all controller methods.
- @AfterReturning runs after a method successfully returns.
- Builds an AuditLog entry from method name and arguments.
- Persists the audit log using JPA repository.
- Prints a log message on success or failure.
- @Aspect defines this class as an aspect.
- @Pointcut selects which methods to intercept (here, all controller methods).
- Saves all audit details to AUDIT_LOG.
Step 6: Asynchronous Configuration
AsyncConfig.java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "auditExecutor")
public Executor auditExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("audit-");
executor.initialize();
return executor;
}
Explanation
- Configures a thread pool for background tasks (audit logging can be made async).
- Improves performance for high-traffic APIs.
Step 7: Optional — HTTP Request/Response Caching
CachingRequestResponseFilter.java
@Component
public class CachingRequestResponseFilter implements Filter {
public static final String CORRELATION_ID_HEADER = "X-Correlation-Id";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
wrappedResponse.setHeader(CORRELATION_ID_HEADER, correlationId);
long start = System.currentTimeMillis();
chain.doFilter(wrappedRequest, wrappedResponse);
long duration = System.currentTimeMillis() - start;
request.setAttribute("audit.correlationId", correlationId);
request.setAttribute("audit.durationMs", duration);
wrappedResponse.copyBodyToResponse();
}
}
Explanation
- Adds a X-Correlation-Id header for tracing.
- Measures execution time for each request.
- Enhances observability when debugging logs.
- Adds both as request attributes so they can appear in audit details.
- Returns them in the HTTP response headers.
Step 8: Database Setup
schema.sql
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
method VARCHAR(255),
endpoint VARCHAR(500),
http_method VARCHAR(50),
status VARCHAR(255),
execution_time_ms BIGINT,
timestamp TIMESTAMP
);
data.sql
-- Insert sample products
INSERT INTO product (name, category, price) VALUES ('iPhone 15', 'Electronics', 1299.99);
INSERT INTO product (name, category, price) VALUES ('Samsung Galaxy S24', 'Electronics', 1199.50);
INSERT INTO product (name, category, price) VALUES ('MacBook Pro 14"', 'Computers', 2499.00);
INSERT INTO product (name, category, price) VALUES ('Dell XPS 13', 'Computers', 1399.00);
INSERT INTO product (name, category, price) VALUES ('Sony WH-1000XM5', 'Accessories', 399.99);
INSERT INTO product (name, category, price) VALUES ('Apple Watch Ultra 2', 'Wearables', 999.00);
INSERT INTO product (name, category, price) VALUES ('Logitech MX Master 3S', 'Accessories', 149.99);
Step 9: Application Properties
application.properties
spring.datasource.url=jdbc:h2:mem:auditdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
#spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
logging.level.org.springframework.web=INFO
spring.security.user.name=admin
spring.security.user.password=admin123
# ============= JPA / Hibernate =============
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.defer-datasource-initialization=true
# ============= SQL Initialization =============
spring.sql.init.mode=always
Running the Application
Step 1 — Start the app
Run As → Java Application
Step 2 — Send requests
- Create a product
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Laptop","category":"Electronics","price":1200}'
- Fetch all products
curl http://localhost:8080/api/products
- Update a product
curl -X PUT http://localhost:8080/api/products/1 \
-H "Content-Type: application/json" \
-d '{"name":"Laptop Pro","category":"Electronics","price":1350}'
- Delete a product
curl -X DELETE http://localhost:8080/api/products/1
Outputs and Explanations
Console Log Output
2025-11-02T23:37:47.372+05:30 INFO 9392 --- [nio-8080-exec-7] c.e.auditlogging.aspect.AuditAspect : ✅ Audit log saved successfully for method: getAllProducts
2025-11-02T23:38:47.591+05:30 INFO 9392 --- [nio-8080-exec-6] c.e.auditlogging.aspect.AuditAspect : ✅ Audit log saved successfully for method: createProduct
2025-11-02T23:39:20.194+05:30 INFO 9392 --- [nio-8080-exec-9] c.e.auditlogging.aspect.AuditAspect : ✅ Audit log saved successfully for method: deleteProduct
Console Log
Explanation
Each line indicates the method name captured by the audit aspect and successful persistence of its record.
Database Table: AUDIT_LOG
|
ID |
TIMESTAMP |
DETAILS |
ACTION |
USERNAME |
|---|---|---|---|---|
|
1 |
2025-11-02 23:37:47.27367 |
Method getAllProducts executed with args [] |
GETALLPRODUCTS |
system |
|
2 |
2025-11-02 23:38:47.59111 |
Method createProduct executed with args [com.example.auditlogging.entity.Product@37a1b59c] |
CREATEPRODUCT |
system |
|
3 |
2025-11-02 23:39:20.194292 |
Method deleteProduct executed with args [2] |
DELETEPRODUCT |
system |
HTTP Response Example
Request
Response
{
"id": 8,
"name": "MacBook Air",
"category": "Laptop",
"price": 1199.99
}
Response Headers
Content-Type: application/json
X-Correlation-Id: 8a41dc7e-f3a9-4b78-9f10-8c239e62a4f4
Explanation:
The response returns the saved product details along with the correlation ID generated by the filter.
Audit Entry for Above Request
|
ID |
TIMESTAMP |
DETAIL |
ACTION |
USERNAME |
|---|---|---|---|---|
|
1 |
2025-11-02 23:38:47.59111 |
Method createProduct executed with args [com.example.auditlogging.entity.Product@37a1b59c] |
CREATEPRODUCT |
system |
Combined Flow Visualization
|
Step |
Component |
What Happens |
Example Output |
|---|---|---|---|
|
1 |
Controller |
POST /api/products executes |
Product created |
|
2 |
Aspect |
Captures method name + args |
CREATEPRODUCT |
|
3 |
Repository |
Saves AuditLog entry |
Row inserted in DB |
|
4 |
Logger |
Prints success message |
Audit log saved successfully... |
|
5 |
Filter |
Adds correlation ID to response |
X-Correlation-Id: <uuid> |
Final Output Summary
After running all four operations (Create, Read, Update, Delete):
Console Output
Audit log saved successfully for method: createProduct
Audit log saved successfully for method: getAllProducts
Audit log saved successfully for method: updateProduct
Audit log saved successfully for method: deleteProduct
Database
Four rows in AUDIT_LOG table representing each action.
Response Header
Each API response includes X-Correlation-Id.
Response Body Example
{
"id": 1,
"name": "Laptop Pro",
"category": "Electronics",
"price": 1350.0
}
Extending It for Real Users
You can easily integrate with Spring Security to capture the actual logged-in username:
String username = SecurityContextHolder.getContext().getAuthentication().getName();
log.setUsername(username);
Conclusion
You now have a fully working audit logging framework in Spring Boot that automatically captures all REST API actions with minimal code.
This approach ensures:
- Centralized audit logging for all REST endpoints
- Non-intrusive — no need to modify each controller
- Easily extendable to capture IP address, headers, or request body
- Ready for production with async logging and correlation IDs
This setup ensures every REST API call leaves a clear trace for debugging and compliance purposes.