Exploring caching strategies and patterns with AWS ElastiCache Serverless
In a enterprise application a cache is a crucial component for storing data sets that would otherwise require significant time to calculate or retrieve from another backend system. It plays a vital role in optimizing performance and efficiency. Using caching can help eliminate the need for multiple round trips to retrieve frequently accessed data, resulting in improved performance. Optimizing performance and reducing application latencies.
Why Caching?
In order to improve performance and avoid reads and writes to the disk, one of the key patterns is to adopt caching to achieve
- Reducing Response Times: Cache allows for faster content retrieval and reduces additional network roundtrips.
- Reduce Network Costs: The caching strategy makes content available within a specific region or VPC by moving it closer to the user, thereby reducing network activity beyond the cache.
- Content Availability: Caches can make data available even in the event of backend system or network failures.
What Not to Cache?
A general guideline is to not cache the data that changes very often (example: data changed/updated within a hour).
Use Case: Frequent Data Updates by Data Producer
If there is frequent update to data, multiple times a data, there will constant need update/refresh cache. Do not use cache in those circumstances.
What to Cache?
Use Case: Non-Frequent Data Updates by Data Producer
1. Consider caching of data, when we there is a need for resource heavy computation before other services can consume.
2. High number of Reads
3. Complex queries to retrieve data that can be consumed by the consumer
4. Avoid network roundtrips to read the same data set and there is no risk of data getting updated.
Tools
- Redis for Cache strategies/patterns.
- API Gateway cache
- Cloud CDN for CDN use cases.
- AWS Dynamo DB cache
What is AWS ElastiCache Serverless?
ElastiCache Serverless is a version of ElastiCache that operates without the need for servers. It is a managed Redis service. Users of AWS can easily set up a Redis service that is fully provisioned, scaled, and managed by AWS. With ElastiCache Serverless, developers no longer need to worry about provisioning for peak traffic, sharding, rebalancing, snapshotting, and other administrative tasks that come with managing a Redis deployment.
Caching patterns
Here are two common caching patterns that can be implemented when using the Microservice architecture pattern.
Cache-Aside
Cache-Aside is a technique commonly used in software development to improve performance and reduce database load. It involves retrieving data from a cache instead of directly querying the database. By implementing Cache-Aside, developers can optimize their applications and provide faster response times to users.
Caching Flow:
- Enter the request criteria to call the GET API.
- Check the request key in the cache, if data found in the cache return the response.
- Else fetch the record from the Database and save the record in the cache and return the response.
Advantage:
- Once the initial read or lazy read is complete, the data is consistently retrieved from the cache, eliminating the need for a database round trip.
- The cache always stores the frequently requested access data.
- The application would be significantly faster, resulting in a significant reduction in the API response time.
Disadvantage
- Due to the implementation of lazy loading, the initial request may experience some delay.
- There is a potential for outdated data if the database is updated directly.
- Not ideal for frequent data updates.
Write-Through Cache
In this pattern, the data source and cache are consistently kept up-to-date. The main strategy of this design pattern ensures that the cache is always up-to-date with any data operation, such as creation, update, or deletion, when there is a successful update in the database.
- Enter the request criteria to call the POST/PUT/DELETE API.
- Update the data in the database and makes use of the Cache manager to store/update/delete the record in the cache.
Advantage:
- Since the cache is always updated, there is no round-trip to find and load data.
- 2. Because there is no cache miss, the application will have no latency when loading data for the first time.
Disadvantage:
- Since the cache is always refreshed upon data modification, it could cause the application to store data that is rarely retrieved.
Step-by-step Implementation
- Create a AWS Elastic cache serverless
a. Goto →ElasticCache
b. Default setting
c. Review
2. Create Spring boot project
prerequisite: java 17 and Spring boot 3.2
a. Please refer the page to install and configure the Amazon Corretto 17
b. Please create a Spring boot Project from spring initializer.
Please refer the pom.xml
c. Application. Properties
spring.application.name=Serverless Redis
cloud.aws.region.static=us-east-1
cloud.aws.region.auto=false
# AWS Elastic CACHE
# ###################
#spring.data.redis.host=<<ElastiCache-URL>>:6379
#spring.data.redis.database=0
spring.data.redis.ssl.enabled=true
spring.data.redis.timeout=60000
logging.level.org.hibernate.SQL=info
spring.jpa.show-sql=true
d. Controller
package com.poc.serverless.redis.user;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.websocket.server.PathParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
@Slf4j
public class UserProfileController {
private final UserProfileService userProfileService;
@GetMapping
public UserProfileDto getUserProfile(@PathParam("email") final String email) {
log.info("UserProfileController::getUserProfile:Enter");
return userProfileService.getUser(email);
}
@PutMapping("/{email}")
public String update(@RequestParam("email") final String email) {
return "ok";
}
@PostMapping
public String add(@RequestBody UserProfileRequest userProfileRequest) {
userProfileService.saveUser(userProfileRequest);
return "ok";
}
@DeleteMapping("/{email}")
public boolean delete(@PathVariable("email") final String email) {
return userProfileService.deleteUser(email);
}
}
e. Service
package com.poc.serverless.redis.user;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import com.poc.serverless.redis.cache.CachingManager;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
private final CachingManager<UserProfileDto> cachingManager;
private final ModelMapper modelMapper;
@Transactional
public void saveUser(UserProfileRequest userProfileRequest) {
log.info("UserProfileService::saveUser:Start");
UserProfile userProfileEntity = modelMapper.map(userProfileRequest, UserProfile.class);
userProfileRepository.save(userProfileEntity);
UserProfileDto userProfileDto = modelMapper.map(userProfileEntity, UserProfileDto.class);
cachingManager.put(userProfileDto.getEmailAddress(), userProfileDto);
log.info("UserProfileService::saveUser:End");
}
public UserProfileDto getUser(String userEmail) {
log.info("UserProfileService::getUser:Start");
UserProfileDto userProfileDto = null;
UserProfile userProfileEntity = null;
userProfileDto = cachingManager.get(userEmail, UserProfileDto.class);
if(userProfileDto==null) {
userProfileEntity = userProfileRepository.getUserProfileByEmailAddress(userEmail);
if(userProfileEntity!=null) {
userProfileDto = modelMapper.map(userProfileEntity, UserProfileDto.class);
cachingManager.put(userProfileDto.getEmailAddress(), userProfileDto);
}
}
log.info("UserProfileService::getUser:End");
return userProfileDto;
}
public boolean deleteUser(String userEmail) {
log.info("UserProfileService::deleteUser:Start");
boolean isDeleted = false;
UserProfile userProfileEntity = userProfileRepository.getUserProfileByEmailAddress(userEmail);
if(userProfileEntity!=null) {
userProfileRepository.delete(userProfileEntity);
cachingManager.remove(userEmail);
isDeleted = true;
}
log.info("UserProfileService::deleteUser:End");
return isDeleted;
}
}
f. Testing
The program can be launched and run inside AWS with EC2 instance or ECS.
Summary:
This blog post covers the relevance of the cache in the enterprise application, caching strategies, advantages and disadvantages, and implementation details of the cache aside and the write-thought cache.