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.
Audit logging records what actions were performed in your application, by whom, and when. \n In a REST API, audit logs are useful for:
We’ll build this with Spring Boot, JPA, and Aspect-Oriented Programming (AOP).
Imagine a Product Management System where multiple users create, update, and delete products through REST APIs.
\n For compliance and debugging, you need to record:
Instead of manually logging in every controller, we’ll use Spring AOP (Aspect-Oriented Programming) to intercept and persist audit logs automatically.
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
\
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); } }
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; } }
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; } }
AuditLogRepository.java @Repository public interface AuditLogRepository extends JpaRepository<AuditLog, Long> { } ProductRepository.java @Repository public interface ProductRepository extends JpaRepository<Product, Long> {}
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); } }
This controller exposes four REST endpoints:
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(); } } }
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; }
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(); } }
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);
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
Run As → Java Application
curl -X POST http://localhost:8080/api/products \ -H "Content-Type: application/json" \ -d '{"name":"Laptop","category":"Electronics","price":1200}'
curl http://localhost:8080/api/products
curl -X PUT http://localhost:8080/api/products/1 \ -H "Content-Type: application/json" \ -d '{"name":"Laptop Pro","category":"Electronics","price":1350}'
curl -X DELETE http://localhost:8080/api/products/1
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
Each line indicates the method name captured by the audit aspect and successful persistence of its record.
| 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 |
{ "id": 8, "name": "MacBook Air", "category": "Laptop", "price": 1199.99 }
Content-Type: application/json X-Correlation-Id: 8a41dc7e-f3a9-4b78-9f10-8c239e62a4f4
The response returns the saved product details along with the correlation ID generated by the filter.
| 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 |
| 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: |
After running all four operations (Create, Read, Update, Delete):
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
Four rows in AUDIT_LOG table representing each action.
Each API response includes X-Correlation-Id.
{ "id": 1, "name": "Laptop Pro", "category": "Electronics", "price": 1350.0 }
You can easily integrate with Spring Security to capture the actual logged-in username:
String username = SecurityContextHolder.getContext().getAuthentication().getName(); log.setUsername(username);
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:
This setup ensures every REST API call leaves a clear trace for debugging and compliance purposes.
\


