Pratyush Majumdar
Pratyush Majumdar Technology Enthusiast, Performance Specialist and Cloud Architect

API Performance Optimisation Best Practices

API Performance Optimisation Best Practices

1. Implement effective caching: Utilize server-side and client-side caching (e.g., Redis) and leverage HTTP cache headers.

This Spring Boot code demonstrates effective caching by combining server-side caching using Redis and client-side caching using HTTP cache headers.

At the server side, Spring’s caching abstraction is enabled using @EnableCaching. The @Cacheable annotation in the service layer stores the result of a method call in Redis. When a request for the same data is made again, Spring first checks Redis and returns the cached value instead of executing the method (e.g., avoiding a database call). A TTL (time-to-live) is configured so cached data automatically expires after a defined duration.

At the client side, the REST controller adds HTTP cache headers such as Cache-Control and ETag to the response. Cache-Control allows browsers or CDNs to cache the response for a specified time, while ETag enables conditional requests. If the data has not changed, the server can respond with 304 Not Modified, reducing bandwidth and improving performance.

1.Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Cache -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Jackson (for Redis serialization) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

2.Enable Caching

1
2
3
4
5
6
7
@SpringBootApplication
@EnableCaching
public class CachingApplication {
    public static void main(String[] args) {
        SpringApplication.run(CachingApplication.class, args);
    }
}

3.Redis Configuration

1
2
3
4
5
6
7
8
9
10
@Configuration
public class RedisConfig {

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // cache TTL
                .disableCachingNullValues();
    }
}

4.Service Layer (Server-Side Caching)

  • First call will hit the DB.
  • Next call will be served from Redis.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        simulateSlowCall();
        return new Product(id, "Product-" + id, 100.0);
    }

    private void simulateSlowCall() {
        try {
            Thread.sleep(2000); // simulate DB call
        } catch (InterruptedException ignored) {}
    }
}

5.REST Controller (Client-Side Caching via HTTP Headers)

  • Cache-Control: public, max-age=60
  • Browser/CDN caches response for 60 seconds
  • ETag enables conditional requests (304 Not Modified)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product product = productService.getProductById(id);

        String eTag = Integer.toHexString(product.hashCode());

        return ResponseEntity.ok()
                .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic())
                .eTag(eTag)
                .body(product);
    }
}

6.Model Class (Java 17)

1
public record Product(Long id, String name, Double price) {}

7.application.properties

1
2
spring.redis.host=localhost
spring.redis.port=6379

2. Use asynchronous processing: Offload long-running tasks via queues and background workers to keep endpoints responsive.

To keep REST APIs responsive while handling long-running tasks, we use RabbitMQ as a message queue. Instead of processing heavy work inside the HTTP request thread, the application publishes a message to a RabbitMQ queue and returns the response immediately. A background consumer then processes the task asynchronously.

Working

  1. The REST controller receives a request and creates a task message.
  2. The message is published to a durable RabbitMQ queue using RabbitTemplate.
  3. The API responds immediately with 202 Accepted.
  4. A RabbitMQ consumer (@RabbitListener) listens to the queue and processes messages in the background.
  5. RabbitMQ handles buffering, retries, and scalability, ensuring tasks are not lost.

This approach decouples API responsiveness from task execution, supports horizontal scaling, survives application restarts, and is suitable for production workloads such as file processing, notifications, and integrations.

RabbitMQ Architecture

1.Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.RabbitMQ configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class RabbitMQConfig {

    public static final String QUEUE_NAME = "long-running-task-queue";

    @Bean
    public Queue taskQueue() {
        return new Queue(QUEUE_NAME, true);
    }

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

3.Message payload

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TaskMessage {
    private String taskId;
}

4.Producer (publishes tasks to RabbitMQ)

1
2
3
4
5
6
7
8
9
10
@Service
@RequiredArgsConstructor
public class TaskProducer {

    private final RabbitTemplate rabbitTemplate;

    public void send(TaskMessage message) {
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, message);
    }
}

5.Consumer (background worker)

1
2
3
4
5
6
7
8
9
10
@Service
public class TaskConsumer {

    @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
    public void process(TaskMessage message) throws InterruptedException {
        System.out.println("Processing task: " + message.getTaskId());
        Thread.sleep(5000);
        System.out.println("Completed task: " + message.getTaskId());
    }
}

6.REST Controller (fast and responsive endpoint)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/tasks")
@RequiredArgsConstructor
public class TaskController {

    private final TaskProducer producer;

    @PostMapping("/start")
    public ResponseEntity<String> startTask() {
        String taskId = UUID.randomUUID().toString();
        producer.send(TaskMessage.builder().taskId(taskId).build());
        return ResponseEntity.accepted().body("Task queued: " + taskId);
    }
}

7.application.properties

1
2
3
4
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

comments powered by Disqus