├── .gitignore ├── LICENSE ├── README.md ├── java-caching-poc ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── attia │ │ │ └── cachingpoc │ │ │ ├── CachingPocApplication.java │ │ │ ├── config │ │ │ └── RedisConfig.java │ │ │ ├── controller │ │ │ └── ProductController.java │ │ │ ├── entity │ │ │ └── Product.java │ │ │ ├── repository │ │ │ └── ProductRepository.java │ │ │ └── service │ │ │ └── ProductService.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── attia │ └── cachingpoc │ └── CachingPocApplicationTests.java └── slides └── Scaling Your App with Backend Caching Strategies.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | /.idea/ 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Muhammad Mahmoud Attia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scaling Your App with Backend Caching Strategies Workshop 2 | Workshop Materials: https://github.com/MuhammadAttia/caching-workshop 3 | 4 | Muhammad Attia - Lead Software Engineer @ ELM 5 | 6 | 7 | ## Contribution 8 | If you want to contribute by implementing the same POC using different programming languages such as Golang, PHP, C#, and Node.js,...etc feel free to make a new folder or module and prefix it with the programming language. For example, `go-caching-poc` then opens a pull request to review. 9 | 10 | ## Doing the Workshop on Your Own 11 | 12 | ### Problem Statment 13 | In an e-commerce platform product catalog. often experience heavy read traffic as users browse and search for products, and product details may be updated periodically. so we need to use the cache alongside other techniques to enhance the Service performance. 14 | 15 | 16 | ### Solution 17 | 18 | In this scenario, read-aside caching is used to optimize the read-heavy operations, quickly serving users with frequently accessed data from the cache. Write-through caching ensures that data consistency is maintained when product updates occur, preventing the cache from serving outdated information such as price and stock availability. 19 | This combination allows the e-commerce platform to deliver a responsive user experience while keeping the product catalog data updated. It balances between optimizing read performance and maintaining data integrity, making it a suitable strategy for this real-life use case. 20 | 21 | This guide will help you set up a Spring Boot project that uses Spring Data, Redis for caching, and H2 as an in-memory database. We'll create a simple product catalog management application implementing product listings and update product details using read-aside and write-through caching strategies. 22 | 23 | ## Prerequisites 24 | Everyone will need: 25 | 26 | - Basic knowledge of Java, and Spring boot (Basics ). 27 | 28 | - [JDK 17 or higher](https://openjdk.java.net/install/index.html) installed. **Ensure you have a JDK installed and not just a JRE** 29 | - [docker](https://docs.docker.com/install/) installed. 30 | - [redis](https://hub.docker.com/_/redis) installed. 31 | - [maven](https://maven.apache.org/install.html) installed. 32 | 33 | 34 | ## Step 1: Set Up the Development Environment 35 | 36 | - **Install Java:** Ensure you have Java 17 or later installed on your system. 37 | 38 | - **Install Maven:** Install Apache Maven as your build tool. Download it from the [Apache Maven website](https://maven.apache.org/download.cgi). 39 | 40 | ## Step 2: Create a Spring Boot Project 41 | 42 | 1. **Use Spring Initializer:** 43 | 44 | - Visit [Spring Initializer](https://start.spring.io/) / open IntelliJ 45 | - Choose project type: Maven Project. 46 | - Language: Java. 47 | - Spring Boot version: Latest stable version. 48 | - Group: com.attia. 49 | - Artifact: product-management. 50 | - Add dependencies: "Spring Web," "Spring Data JPA," "H2 Database," Lombok ", and "Spring Data Redis." 51 | 52 | 2. **Generate Project:** 53 | 54 | Click the "Generate" button and download the generated project ZIP file. 55 | 56 | 3. **Extract the Project:** 57 | 58 | Extract the downloaded ZIP file to a directory of your choice. 59 | 60 | ## Step 3: Configure Redis for Caching 61 | 62 | 1. **Install Redis:** 63 | 64 | Download and install Redis on your system. Follow installation instructions for your platform on the [official Redis website](https://redis.io/download). 65 | 66 | 2. **Configure Redis for Your Spring Boot Application:** 67 | 68 | Open your project's `src/main/resources/application.yml` file and add the following Redis and H2 configuration: 69 | 70 | ```yml 71 | spring: 72 | data: 73 | redis: 74 | host: localhost 75 | port: 6379 76 | datasource: 77 | url: jdbc:h2:mem:product-db 78 | driverClassName: org.h2.Driver 79 | username: sa 80 | password: password 81 | h2: 82 | console: 83 | enabled: true 84 | path: /h2-console 85 | 86 | ``` 87 | 3. *** Add Redis Config 88 | create config package and on this package add the Redis config class 89 | ```java 90 | @Configuration 91 | public class RedisConfig { 92 | 93 | private static final String PRODUCT_CACHE_KEY = "products"; 94 | 95 | private final ProductRepository productRepository; 96 | 97 | 98 | public RedisConfig(ProductRepository productRepository) { 99 | this.productRepository = productRepository; 100 | } 101 | 102 | 103 | @Bean 104 | public RMapCache productRMapCache(RedissonClient redissonClient) { 105 | return redissonClient.getMapCache(PRODUCT_CACHE_KEY, 106 | MapOptions.defaults() 107 | .writer(getMapWriter()) 108 | .writeMode(MapOptions.WriteMode.WRITE_THROUGH)); 109 | } 110 | @Bean 111 | public RedissonClient redissonClient() { 112 | final Config config = new Config(); 113 | config.setCodec(new JsonJacksonCodec()); 114 | config.useSingleServer() 115 | .setAddress("redis://localhost:6379"); 116 | return Redisson.create(config); 117 | } 118 | 119 | 120 | private MapWriter getMapWriter() { 121 | return new MapWriter<>() { 122 | @Override 123 | public void write(final Map map) { 124 | map.forEach((k, v) -> { 125 | // Update the database here 126 | updateDatabase(v); 127 | }); 128 | } 129 | 130 | @Override 131 | public void delete(Collection keys) { 132 | // Delete from the database here 133 | keys.forEach(productRepository::deleteById); 134 | } 135 | }; 136 | } 137 | 138 | private void updateDatabase(Product product) { 139 | // Perform the necessary database update logic here 140 | productRepository.save(product); 141 | } 142 | } 143 | ``` 144 | ## Step 4: update the pom file 145 | 146 | ```xml 147 | 148 | 149 | 151 | 4.0.0 152 | 153 | org.springframework.boot 154 | spring-boot-starter-parent 155 | 3.1.5 156 | 157 | 158 | com.attia 159 | product-management 160 | 0.0.1-SNAPSHOT 161 | product-management 162 | product-management 163 | 164 | 21 165 | 166 | 167 | 168 | org.springframework.boot 169 | spring-boot-starter-data-jpa 170 | 171 | 172 | org.springframework.boot 173 | spring-boot-starter-data-redis 174 | 175 | 176 | 177 | org.springframework.boot 178 | spring-boot-starter-web 179 | 180 | 181 | 182 | com.h2database 183 | h2 184 | runtime 185 | 186 | 187 | org.projectlombok 188 | lombok 189 | true 190 | 191 | 192 | org.springframework.boot 193 | spring-boot-starter-test 194 | test 195 | 196 | 197 | 198 | org.springframework.boot 199 | spring-boot-starter-actuator 200 | 201 | 202 | 203 | 204 | org.redisson 205 | redisson-spring-boot-starter 206 | 3.16.3 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | org.springframework.boot 215 | spring-boot-maven-plugin 216 | 217 | 218 | 219 | org.project-lombok 220 | lombok 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | ``` 231 | 232 | ## Step 5: Create the Product Entity 233 | 234 | Create a Product entity that represents the product details: 235 | 236 | ```java 237 | @Getter 238 | @Setter 239 | @AllArgsConstructor 240 | @Entity 241 | @NoArgsConstructor 242 | public class Product implements Serializable { 243 | @Id 244 | @GeneratedValue(strategy = GenerationType.IDENTITY) 245 | private Long id; 246 | 247 | private String name; 248 | 249 | private String description; 250 | 251 | private double price; 252 | 253 | private boolean available; 254 | } 255 | ``` 256 | ## Step 6: Create a Product Repository 257 | Create a repository interface for accessing the product data: 258 | 259 | ```java 260 | @Repository 261 | public interface ProductRepository extends JpaRepository { 262 | } 263 | ``` 264 | ## Step 7: Implement Product Service 265 | Create a ProductService that handles both read and write operations. We will use write-through caching for product updates. 266 | 267 | ```java 268 | 269 | @Service 270 | public class ProductService { 271 | 272 | private final ProductRepository productRepository; 273 | 274 | private final RMapCache productRMapCache; 275 | 276 | 277 | public ProductService(ProductRepository productRepository , RMapCache productRMapCache) { 278 | this.productRepository = productRepository; 279 | this.productRMapCache = productRMapCache; 280 | } 281 | 282 | // Cache-Aside -> Read from cache; if there is a miss, go to DB 283 | public List getAllProducts() { 284 | List cachedProducts = (List) productRMapCache.readAllValues(); 285 | 286 | // Check cache 287 | if (cachedProducts != null && !cachedProducts.isEmpty()) { 288 | return cachedProducts; 289 | } 290 | 291 | // Get products from DB 292 | List products = getProductsFromDB(); 293 | 294 | // Cache the product listings with an expiration time 295 | setProductsInCache(products); 296 | 297 | return products; 298 | } 299 | 300 | public void setProductsInCache(List products) { 301 | // Set the list of products in the cache with a specified key and expiration time 302 | products.forEach(product -> { 303 | productRMapCache.put(product.getId(), product); 304 | 305 | // Add slight jitter to the expiration time (e.g., within 10% of the original duration) 306 | // Calculate jitter in milliseconds (e.g., within 10% of the original duration) 307 | Duration originalDuration = Duration.ofMinutes(10); 308 | Duration jitter = Duration.ofMinutes((long) (originalDuration.toMinutes() * 0.1 * Math.random())); 309 | Duration duration = originalDuration.plus(jitter); 310 | 311 | // Set the product expiration with jitter 312 | productRMapCache.expire(Instant.now().plus(duration)); 313 | 314 | 315 | }); 316 | } 317 | 318 | public List getProductsFromDB() { 319 | // Simulate fetching products from the database 320 | try { 321 | Thread.sleep(5000); 322 | } catch (InterruptedException e) { 323 | throw new RuntimeException(e); 324 | } 325 | return productRepository.findAll(); 326 | } 327 | 328 | // Write-Through -> Write on cache then write to DB 329 | public Product updateProduct(Product product) { 330 | updateProductInCache(product); 331 | return product; 332 | } 333 | 334 | private void updateProductInCache(Product product) { 335 | // This will automatically trigger the MapWriter to write to the database 336 | productRMapCache.put(product.getId(), product); 337 | } 338 | 339 | public Optional getProductById(Long productId) { 340 | return Optional.ofNullable(productRMapCache.get(productId)); 341 | } 342 | } 343 | 344 | 345 | ``` 346 | ## Step 8: Create REST API Endpoints 347 | Create REST API endpoints to retrieve product listings and update product details: 348 | 349 | ```java 350 | @RestController 351 | @RequestMapping("/products") 352 | public class ProductController { 353 | private final ProductService productService; 354 | 355 | public ProductController(ProductService productService) { 356 | this.productService = productService; 357 | } 358 | 359 | @GetMapping 360 | public List getAllProducts() { 361 | return productService.getAllProducts(); 362 | } 363 | 364 | @PutMapping("/{productId}") 365 | public ResponseEntity updateProduct(@PathVariable Long productId, @RequestBody Product updatedProduct) { 366 | Optional productOptional = productService.getProductById(productId); 367 | 368 | if (productOptional.isEmpty()) { 369 | return ResponseEntity.notFound().build(); 370 | } 371 | 372 | Product product = productOptional.get(); 373 | 374 | // Update the product details 375 | product.setName(updatedProduct.getName()); 376 | product.setDescription(updatedProduct.getDescription()); 377 | product.setPrice(updatedProduct.getPrice()); 378 | product.setAvailable(updatedProduct.isAvailable()); 379 | 380 | // Save the updated product 381 | Product updated = productService.updateProduct(product); 382 | 383 | return ResponseEntity.ok(updated); 384 | } 385 | 386 | } 387 | ``` 388 | ## Step 9 : Update the main class by adding some products to DB for testing purposes 389 | 390 | ```java 391 | @SpringBootApplication 392 | @EnableCaching 393 | public class ProductManagementApplication implements CommandLineRunner { 394 | 395 | private final ProductRepository productRepository; 396 | 397 | public ProductManagementApplication(ProductRepository productRepository) { 398 | this.productRepository = productRepository; 399 | } 400 | 401 | public static void main(String[] args) { 402 | SpringApplication.run(ProductManagementApplication.class, args); 403 | } 404 | 405 | @Override 406 | public void run(String... args) throws Exception { 407 | insertSampleProducts(); 408 | } 409 | 410 | private void insertSampleProducts() { 411 | // Create 10 sample products 412 | List sampleProducts = Arrays.asList( 413 | new Product(1L, "Laptop", "Powerful laptop with high performance", 1200.0, true), 414 | new Product(2L, "Smartphone", "Latest smartphone with advanced features", 800.0, true), 415 | new Product(3L, "Headphones", "Wireless over-ear headphones with noise cancellation", 150.0, true), 416 | new Product(4L, "Smartwatch", "Fitness and health tracking smartwatch", 200.0, true), 417 | new Product(5L, "Tablet", "Lightweight and portable tablet", 400.0, true), 418 | new Product(6L, "Gaming Console", "Next-gen gaming console for immersive gaming experience", 500.0, true), 419 | new Product(7L, "Camera", "High-resolution digital camera for professional photography", 1000.0, true), 420 | new Product(8L, "Wireless Router", "High-speed wireless router for seamless internet connectivity", 80.0, true), 421 | new Product(9L, "Bluetooth Speaker", "Portable Bluetooth speaker with rich sound quality", 50.0, true), 422 | new Product(10L, "External Hard Drive", "Large capacity external hard drive for data storage", 120.0, true) 423 | ); 424 | // Insert some products into the database 425 | productRepository.saveAll(sampleProducts); 426 | } 427 | } 428 | ``` 429 | ## Step 10: Build and Run the Application 430 | Start your Spring Boot application, and it will be accessible at http://localhost:8080, and DB on http://localhost:8080/h2-console 431 | 432 | ## Testing 433 | Use tools like Postman or Curl to make GET requests to retrieve product listings and PUT requests to update product details. 434 | 435 | Monitor the Redis cache to see how data is cached (you can use Redis CLI or Another Redis software Download it from the [Another Redis software](https://github.com/qishibo/AnotherRedisDesktopManager) ), and observe that changes are reflected in the cache and the database. 436 | 437 | This example demonstrates a simplified implementation of an e-commerce product catalog using Spring Boot and Redis with read-aside and write-through backend caching strategies. 438 | -------------------------------------------------------------------------------- /java-caching-poc/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | /.mvn/ 35 | -------------------------------------------------------------------------------- /java-caching-poc/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.1.5 9 | 10 | 11 | com.attia 12 | caching-poc 13 | 0.0.1-SNAPSHOT 14 | caching-poc 15 | caching-poc 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-redis 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | com.h2database 36 | h2 37 | runtime 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | true 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-actuator 53 | 54 | 55 | 56 | 57 | org.redisson 58 | redisson-spring-boot-starter 59 | 3.16.3 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 70 | 71 | 72 | org.project-lombok 73 | lombok 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /java-caching-poc/src/main/java/com/attia/cachingpoc/CachingPocApplication.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc; 2 | 3 | import com.attia.cachingpoc.entity.Product; 4 | import com.attia.cachingpoc.repository.ProductRepository; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.cache.annotation.EnableCaching; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | @SpringBootApplication 14 | @EnableCaching 15 | public class CachingPocApplication implements CommandLineRunner { 16 | 17 | private final ProductRepository productRepository; 18 | 19 | public CachingPocApplication(ProductRepository productRepository) { 20 | this.productRepository = productRepository; 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(CachingPocApplication.class, args); 25 | } 26 | 27 | @Override 28 | public void run(String... args) { 29 | insertSampleProducts(); 30 | } 31 | 32 | private void insertSampleProducts() { 33 | // Create 10 sample products 34 | List sampleProducts = Arrays.asList( 35 | new Product(1L, "Laptop", "Powerful laptop with high performance", 1200.0, true), 36 | new Product(2L, "Smartphone", "Latest smartphone with advanced features", 800.0, true), 37 | new Product(3L, "Headphones", "Wireless over-ear headphones with noise cancellation", 150.0, true), 38 | new Product(4L, "Smartwatch", "Fitness and health tracking smartwatch", 200.0, true), 39 | new Product(5L, "Tablet", "Lightweight and portable tablet", 400.0, true), 40 | new Product(6L, "Gaming Console", "Next-gen gaming console for immersive gaming experience", 500.0, true), 41 | new Product(7L, "Camera", "High-resolution digital camera for professional photography", 1000.0, true), 42 | new Product(8L, "Wireless Router", "High-speed wireless router for seamless internet connectivity", 80.0, true), 43 | new Product(9L, "Bluetooth Speaker", "Portable Bluetooth speaker with rich sound quality", 50.0, true), 44 | new Product(10L, "External Hard Drive", "Large capacity external hard drive for data storage", 120.0, true) 45 | ); 46 | // Insert some products into the database 47 | productRepository.saveAll(sampleProducts); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /java-caching-poc/src/main/java/com/attia/cachingpoc/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc.config; 2 | 3 | import com.attia.cachingpoc.entity.Product; 4 | import com.attia.cachingpoc.repository.ProductRepository; 5 | import org.redisson.Redisson; 6 | import org.redisson.api.MapOptions; 7 | import org.redisson.api.RMapCache; 8 | import org.redisson.api.RedissonClient; 9 | import org.redisson.api.map.MapWriter; 10 | import org.redisson.codec.JsonJacksonCodec; 11 | import org.redisson.config.Config; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | import java.util.Collection; 16 | import java.util.Map; 17 | 18 | @Configuration 19 | public class RedisConfig { 20 | 21 | private static final String PRODUCT_CACHE_KEY = "products"; 22 | 23 | private final ProductRepository productRepository; 24 | 25 | 26 | public RedisConfig(ProductRepository productRepository) { 27 | this.productRepository = productRepository; 28 | } 29 | 30 | 31 | @Bean 32 | public RMapCache productRMapCache(RedissonClient redissonClient) { 33 | return redissonClient.getMapCache(PRODUCT_CACHE_KEY, 34 | MapOptions.defaults() 35 | .writer(getMapWriter()) 36 | .writeMode(MapOptions.WriteMode.WRITE_THROUGH)); 37 | } 38 | @Bean 39 | public RedissonClient redissonClient() { 40 | final Config config = new Config(); 41 | config.setCodec(new JsonJacksonCodec()); 42 | config.useSingleServer() 43 | .setAddress("redis://localhost:6379"); 44 | return Redisson.create(config); 45 | } 46 | 47 | 48 | private MapWriter getMapWriter() { 49 | return new MapWriter<>() { 50 | @Override 51 | public void write(final Map map) { 52 | map.forEach((k, v) -> { 53 | // Update the database here 54 | updateDatabase(v); 55 | }); 56 | } 57 | 58 | @Override 59 | public void delete(Collection keys) { 60 | // Delete from the database here 61 | keys.forEach(productRepository::deleteById); 62 | } 63 | }; 64 | } 65 | 66 | private void updateDatabase(Product product) { 67 | // Perform the necessary database update logic here 68 | productRepository.save(product); 69 | } 70 | } -------------------------------------------------------------------------------- /java-caching-poc/src/main/java/com/attia/cachingpoc/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc.controller; 2 | 3 | import com.attia.cachingpoc.entity.Product; 4 | import com.attia.cachingpoc.service.ProductService; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | @RestController 12 | @RequestMapping("/products") 13 | public class ProductController { 14 | private final ProductService productService; 15 | 16 | public ProductController(ProductService productService) { 17 | this.productService = productService; 18 | } 19 | 20 | @GetMapping 21 | public List getAllProducts() { 22 | return productService.getAllProducts(); 23 | } 24 | 25 | @PutMapping("/{productId}") 26 | public ResponseEntity updateProduct(@PathVariable Long productId, @RequestBody Product updatedProduct) { 27 | Optional productOptional = productService.getProductById(productId); 28 | 29 | if (productOptional.isEmpty()) { 30 | return ResponseEntity.notFound().build(); 31 | } 32 | 33 | Product product = productOptional.get(); 34 | 35 | // Update the product details 36 | product.setName(updatedProduct.getName()); 37 | product.setDescription(updatedProduct.getDescription()); 38 | product.setPrice(updatedProduct.getPrice()); 39 | product.setAvailable(updatedProduct.isAvailable()); 40 | 41 | // Save the updated product 42 | Product updated = productService.updateProduct(product); 43 | 44 | return ResponseEntity.ok(updated); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /java-caching-poc/src/main/java/com/attia/cachingpoc/entity/Product.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc.entity; 2 | import jakarta.persistence.Entity; 3 | import jakarta.persistence.GeneratedValue; 4 | import jakarta.persistence.GenerationType; 5 | import jakarta.persistence.Id; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | 11 | import java.io.Serializable; 12 | 13 | 14 | @Getter 15 | @Setter 16 | @AllArgsConstructor 17 | @Entity 18 | @NoArgsConstructor 19 | public class Product implements Serializable { 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | private String name; 25 | 26 | private String description; 27 | 28 | private double price; 29 | 30 | private boolean available; 31 | } 32 | -------------------------------------------------------------------------------- /java-caching-poc/src/main/java/com/attia/cachingpoc/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc.repository; 2 | 3 | import com.attia.cachingpoc.entity.Product; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface ProductRepository extends JpaRepository { 9 | } -------------------------------------------------------------------------------- /java-caching-poc/src/main/java/com/attia/cachingpoc/service/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc.service; 2 | 3 | import com.attia.cachingpoc.entity.Product; 4 | import com.attia.cachingpoc.repository.ProductRepository; 5 | import org.redisson.api.RMapCache; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | import java.time.Instant; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @Service 14 | public class ProductService { 15 | 16 | private final ProductRepository productRepository; 17 | 18 | private final RMapCache productRMapCache; 19 | 20 | 21 | public ProductService(ProductRepository productRepository , RMapCache productRMapCache) { 22 | this.productRepository = productRepository; 23 | this.productRMapCache = productRMapCache; 24 | } 25 | 26 | // Cache-Aside -> Read from cache; if there is a miss, go to DB 27 | public List getAllProducts() { 28 | List cachedProducts = (List) productRMapCache.readAllValues(); 29 | 30 | // Check cache 31 | if (cachedProducts != null && !cachedProducts.isEmpty()) { 32 | return cachedProducts; 33 | } 34 | 35 | // Get products from DB 36 | List products = getProductsFromDB(); 37 | 38 | // Cache the product listings with an expiration time 39 | setProductsInCache(products); 40 | 41 | return products; 42 | } 43 | 44 | public void setProductsInCache(List products) { 45 | // Set the list of products in the cache with a specified key and expiration time 46 | products.forEach(product -> { 47 | productRMapCache.put(product.getId(), product); 48 | 49 | // Add slight jitter to the expiration time (e.g., within 10% of the original duration) 50 | // Calculate jitter in milliseconds (e.g., within 10% of the original duration) 51 | Duration originalDuration = Duration.ofMinutes(10); 52 | Duration jitter = Duration.ofMinutes((long) (originalDuration.toMinutes() * 0.1 * Math.random())); 53 | Duration duration = originalDuration.plus(jitter); 54 | 55 | // Set the product expiration with jitter 56 | productRMapCache.expire(Instant.now().plus(duration)); 57 | 58 | 59 | }); 60 | } 61 | 62 | public List getProductsFromDB() { 63 | // Simulate fetching products from the database 64 | try { 65 | Thread.sleep(5000); 66 | } catch (InterruptedException e) { 67 | throw new RuntimeException(e); 68 | } 69 | return productRepository.findAll(); 70 | } 71 | 72 | // Write-Through -> Write on cache then write to DB 73 | public Product updateProduct(Product product) { 74 | updateProductInCache(product); 75 | return product; 76 | } 77 | 78 | private void updateProductInCache(Product product) { 79 | // This will automatically trigger the MapWriter to write to the database after updating cache. 80 | productRMapCache.put(product.getId(), product); 81 | } 82 | 83 | public Optional getProductById(Long productId) { 84 | return Optional.ofNullable(productRMapCache.get(productId)); 85 | } 86 | } -------------------------------------------------------------------------------- /java-caching-poc/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | redis: 4 | host: localhost 5 | port: 6379 6 | datasource: 7 | url: jdbc:h2:mem:product-db 8 | driverClassName: org.h2.Driver 9 | username: sa 10 | password: password 11 | h2: 12 | console: 13 | enabled: true 14 | path: /h2-console 15 | -------------------------------------------------------------------------------- /java-caching-poc/src/test/java/com/attia/cachingpoc/CachingPocApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.attia.cachingpoc; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class CachingPocApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /slides/Scaling Your App with Backend Caching Strategies.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhammadattia95/caching-workshop/c1c55c3cf16b683a83416bf7969d4c17cf34c0f6/slides/Scaling Your App with Backend Caching Strategies.pdf --------------------------------------------------------------------------------