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:

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:

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

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

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

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

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

This controller exposes four REST endpoints:

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

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

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

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

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

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:

This setup ensures every REST API call leaves a clear trace for debugging and compliance purposes.