Spring Boot Audit Logs: Capture Every API Action Without Writing Boilerplate Code

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 logging records what actions were performed in your application, by whom, and when. n 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.

n 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 

&nbsp;&nbsp;&nbsp;&nbsp; -H "Content-Type: application/json" 

&nbsp;&nbsp;&nbsp;&nbsp; -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 

&nbsp;&nbsp;&nbsp;&nbsp; -H "Content-Type: application/json" 

&nbsp;&nbsp;&nbsp;&nbsp; -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

Console Log Output

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

POSTMAN

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: |

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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.