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.
\

Rich Dad, Poor Dad author Robert Kiyosaki says he prefers accumulating gold, silver, oil, Bitcoin, and Ether, which he deems “hard money.” Rich Dad Poor Dad author Robert Kiyosaki, a strong proponent for Bitcoin, says it is “criminal” that kids are being taught from a young age to work for an inflationary currency while arguing the virtues of Bitcoin. “Go to school, get a job, work hard, save money, and invest in a 401(k) full of garbage,” Kiyosaki said during a podcast hosted by Bitcoin Collective Co-Founder Jordan Walker on Wednesday. Kiyosaki pulled no punches as he lambasted central banks, equating them to “criminal organizations” and even calling them “Marxists,” as he says that every time central banks print money, it makes the rich richer, while the other economic classes suffer.Read more

