Caching & Messaging

Caching

@Configuration
@EnableCaching
class CacheConfig {}
@Service
public class BookService {

    @Cacheable("books")
    public Book byId(long id) {  }                 // called once per id

    @CachePut(value = "books", key = "#book.id")
    public Book update(Book book) {  }             // always runs, refreshes the entry

    @CacheEvict(value = "books", key = "#id")
    public void delete(long id) {  }

    @CacheEvict(value = "books", allEntries = true)
    public void reload() {  }
}

Keys default to the method arguments; key = "#id" or a SpEL expression overrides that.

Without a cache library you get a simple ConcurrentHashMap. For anything real, pick one:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
spring.cache.cache-names=books
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=10m

Shared across instances? Use Redis:

spring.cache.type=redis
spring.data.redis.host=localhost
spring.data.redis.time-to-live=10m
Warning

Cache only what is expensive and stable. Every cache adds a staleness window and a new class of bug: wrong data that looks right.

Messaging

Messages decouple producer and consumer, absorb load spikes, and survive a restart of the receiver.

Kafka

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=demo
spring.kafka.consumer.auto-offset-reset=earliest
@Service
class BookEvents {
    private final KafkaTemplate<String, BookCreated> template;

    BookEvents(KafkaTemplate<String, BookCreated> template) { this.template = template; }

    void publish(BookCreated event) {
        template.send("books", String.valueOf(event.id()), event);
    }
}

@Component
class BookConsumer {
    @KafkaListener(topics = "books", groupId = "demo")
    void on(BookCreated event) {  }
}

RabbitMQ

spring.rabbitmq.host=localhost
rabbitTemplate.convertAndSend("books.exchange", "book.created", event);

@RabbitListener(queues = "books")
void on(BookCreated event) {  }

JmsClient

Spring 7 adds a fluent JMS client next to JmsTemplate:

jmsClient.destination("notifications").send(event);

Consumer rules

  • idempotent handlers: the same message can arrive twice
  • acknowledge after the work succeeded, not before
  • a dead letter topic for messages that keep failing
  • log the message key with every error, or debugging is guesswork

Local infrastructure

compose.yaml next to pom.xml plus:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-docker-compose</artifactId>
    <optional>true</optional>
</dependency>

./mvnw spring-boot:run now starts the containers and wires the connection details in.

★ Exercises

  1. Add @Cacheable to a slow lookup and measure the second call.
  2. Configure Caffeine with a 1-minute expiry and prove entries disappear.
  3. Evict the cache on update and show a stale read without it.
  4. Publish a BookCreated event to Kafka or RabbitMQ and consume it in the same app.
  5. Make the consumer idempotent — how do you detect a duplicate?