diff --git a/README.md b/README.md
index e21d5be..f9cb5fc 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Add the following dependency to your `pom.xml`:
com.yourcompany.library
common-library
- 1.0-SNAPSHOT
+ 1.2.10-SNAPSHOT
```
diff --git a/am-common-cache-service/README.md b/am-common-cache-service/README.md
new file mode 100644
index 0000000..0f6485d
--- /dev/null
+++ b/am-common-cache-service/README.md
@@ -0,0 +1,92 @@
+# Redis Service Module
+
+This module provides Redis caching functionality for the Portfolio Management System. It handles caching of portfolio data, market indices, stock prices, and other related information.
+
+## Features
+
+- Caching for Portfolio Analysis and Summary
+- Stock Price Caching (Real-time and Historical)
+- Market Index Data Caching
+- Portfolio Holdings Cache Management
+- Redis Health Monitoring
+- Redis Metrics Collection
+
+## Configuration
+
+The module uses Spring Boot's configuration properties. Key configurations can be set in `application.yml`:
+
+```yaml
+spring:
+ data:
+ redis:
+ redisendpoint: localhost:6379
+ portfolio-mover:
+ ttl: 300 # 5 minutes
+ key-prefix: "portfolio:mover:"
+ portfolio-summary:
+ ttl: 300 # 5 minutes
+ key-prefix: "portfolio:summary:"
+ # ... other configurations
+```
+
+## Services
+
+- `StockPriceRedisService`: Manages stock price caching
+- `MarketIndexIndicesRedisService`: Handles market index data caching
+- `PortfolioAnalysisRedisService`: Caches portfolio analysis data
+- `PortfolioHoldingsRedisService`: Manages portfolio holdings cache
+- `PortfolioSummaryRedisService`: Handles portfolio summary caching
+
+## Health Monitoring
+
+The module includes a Redis health indicator that monitors:
+- Redis connection status
+- Redis version
+- Redis mode
+- Operation metrics
+
+## Metrics
+
+The following metrics are collected:
+- Cache hits/misses
+- Operation timing
+- Redis connection status
+
+## Usage
+
+To use this module in your Spring Boot application:
+
+1. Add the module dependency to your `pom.xml`:
+```xml
+
+ com.portfolio
+ redis-service
+ ${project.version}
+
+```
+
+2. The module will auto-configure itself through Spring Boot's auto-configuration mechanism.
+
+3. Inject and use the required services:
+```java
+@Autowired
+private StockPriceRedisService stockPriceRedisService;
+
+@Autowired
+private PortfolioSummaryRedisService portfolioSummaryRedisService;
+```
+
+## Error Handling
+
+The module includes comprehensive error handling and logging:
+- Redis operation exceptions are wrapped in `RedisOperationException`
+- All operations are logged using SLF4J
+- Failed operations are tracked in metrics
+
+## Dependencies
+
+- Spring Boot
+- Spring Data Redis
+- Lettuce Redis Client
+- Lombok
+- Spring Boot Actuator (for metrics and health monitoring)
diff --git a/am-common-cache-service/pom.xml b/am-common-cache-service/pom.xml
new file mode 100644
index 0000000..7952779
--- /dev/null
+++ b/am-common-cache-service/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+
+ com.portfolio
+ am-portfolio
+ 1.2.10-SNAPSHOT
+
+
+ redis-service
+ redis-service
+ Redis Service for Portfolio Management
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ com.am.common
+ am-common-investment-model
+ ${am.investment.version}
+
+
+
+ com.am.common
+ am-common-data-model
+ ${am.common.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+ true
+
+
+
+
+
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/RedisServiceAutoConfiguration.java b/am-common-cache-service/src/main/java/com/portfolio/redis/RedisServiceAutoConfiguration.java
new file mode 100644
index 0000000..51fd60e
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/RedisServiceAutoConfiguration.java
@@ -0,0 +1,13 @@
+package com.portfolio.redis;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+
+import com.portfolio.redis.config.RedisProperties;
+
+@AutoConfiguration
+@ComponentScan(basePackages = "com.portfolio.redis")
+@EnableConfigurationProperties(RedisProperties.class)
+public class RedisServiceAutoConfiguration {
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/annotation/RedisCache.java b/am-common-cache-service/src/main/java/com/portfolio/redis/annotation/RedisCache.java
new file mode 100644
index 0000000..9a1eaf2
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/annotation/RedisCache.java
@@ -0,0 +1,13 @@
+package com.portfolio.redis.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RedisCache {
+ String keyPrefix() default "";
+ long ttl() default 300; // Default TTL of 5 minutes
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/config/RedisConfig.java b/am-common-cache-service/src/main/java/com/portfolio/redis/config/RedisConfig.java
new file mode 100644
index 0000000..e17303d
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/config/RedisConfig.java
@@ -0,0 +1,63 @@
+package com.portfolio.redis.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.portfolio.redis.model.MarketIndexIndicesCache;
+import com.portfolio.redis.model.PortfolioAnalysis;
+import com.portfolio.redis.model.PortfolioHoldings;
+import com.portfolio.redis.model.PortfolioSummaryV1;
+import com.portfolio.redis.model.StockPriceCache;
+
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public RedisTemplate portfolioAnalysisRedisTemplate(RedisConnectionFactory connectionFactory) {
+ return createRedisTemplate(connectionFactory, PortfolioAnalysis.class);
+ }
+
+ @Bean
+ public RedisTemplate portfolioSummaryRedisTemplate(RedisConnectionFactory connectionFactory) {
+ return createRedisTemplate(connectionFactory, PortfolioSummaryV1.class);
+ }
+
+ @Bean
+ public RedisTemplate portfolioHoldingsRedisTemplate(RedisConnectionFactory connectionFactory) {
+ return createRedisTemplate(connectionFactory, PortfolioHoldings.class);
+ }
+
+ @Bean
+ public RedisTemplate stockPriceRedisTemplate(RedisConnectionFactory connectionFactory) {
+ return createRedisTemplate(connectionFactory, StockPriceCache.class);
+ }
+
+ @Bean
+ public RedisTemplate marketIndexIndicesRedisTemplate(RedisConnectionFactory connectionFactory) {
+ return createRedisTemplate(connectionFactory, MarketIndexIndicesCache.class);
+ }
+
+ private RedisTemplate createRedisTemplate(RedisConnectionFactory connectionFactory, Class clazz) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ // Create ObjectMapper with proper configuration
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.findAndRegisterModules();
+
+ // Create serializer using the recommended approach (avoiding deprecated setObjectMapper)
+ Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(mapper, clazz);
+
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(serializer);
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(serializer);
+
+ return template;
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/config/RedisProperties.java b/am-common-cache-service/src/main/java/com/portfolio/redis/config/RedisProperties.java
new file mode 100644
index 0000000..8cc6d90
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/config/RedisProperties.java
@@ -0,0 +1,62 @@
+package com.portfolio.redis.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import lombok.Data;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "spring.data.redis")
+public class RedisProperties {
+ private String redisendpoint;
+ private PortfolioMover portfolioMover = new PortfolioMover();
+ private PortfolioSummary portfolioSummary = new PortfolioSummary();
+ private PortfolioHoldings portfolioHoldings = new PortfolioHoldings();
+ private MarketIndices marketIndices = new MarketIndices();
+ private Stock stock = new Stock();
+
+ @Data
+ public static class PortfolioMover {
+ private Integer ttl;
+ private String keyPrefix;
+ }
+
+ @Data
+ public static class PortfolioSummary {
+ private Integer ttl;
+ private String keyPrefix;
+ }
+
+ @Data
+ public static class PortfolioHoldings {
+ private Integer ttl;
+ private String keyPrefix;
+ }
+
+ @Data
+ public static class MarketIndices {
+ private Integer ttl;
+ private String keyPrefix;
+ private Historical historical = new Historical();
+
+ @Data
+ public static class Historical {
+ private String keyPrefix;
+ private Integer ttl;
+ }
+ }
+
+ @Data
+ public static class Stock {
+ private Integer ttl;
+ private String keyPrefix;
+ private Historical historical = new Historical();
+
+ @Data
+ public static class Historical {
+ private String keyPrefix;
+ private Integer ttl;
+ }
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/exception/RedisOperationException.java b/am-common-cache-service/src/main/java/com/portfolio/redis/exception/RedisOperationException.java
new file mode 100644
index 0000000..abda03b
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/exception/RedisOperationException.java
@@ -0,0 +1,15 @@
+package com.portfolio.redis.exception;
+
+/**
+ * Custom exception for Redis operation failures
+ */
+public class RedisOperationException extends RuntimeException {
+
+ public RedisOperationException(String message) {
+ super(message);
+ }
+
+ public RedisOperationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/BrokerPortfolioSummary.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/BrokerPortfolioSummary.java
new file mode 100644
index 0000000..5dcf281
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/BrokerPortfolioSummary.java
@@ -0,0 +1,25 @@
+package com.portfolio.redis.model;
+
+import java.time.LocalDateTime;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class BrokerPortfolioSummary {
+ private Double investmentValue;
+ private Double currentValue;
+ private Double totalGainLoss;
+ private Double totalGainLossPercentage;
+ private Integer totalAssets;
+ private LocalDateTime lastUpdated;
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/EquityHoldings.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/EquityHoldings.java
new file mode 100644
index 0000000..dbc5beb
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/EquityHoldings.java
@@ -0,0 +1,40 @@
+package com.portfolio.redis.model;
+
+import java.util.List;
+
+import com.am.common.amcommondata.model.enums.BrokerType;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class EquityHoldings {
+ private String isin;
+ private String symbol;
+ private String name;
+ private Double quantity;
+ private Double currentValue;
+ private Double investmentValue;
+ private Double gainLoss;
+ private Double gainLossPercentage;
+ private String sector;
+ private String marketCap;
+ private List brokerPortfolios;
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class EquityBrokerHolding {
+ private BrokerType brokerType;
+ private Double quantity;
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/MarketIndexIndicesCache.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/MarketIndexIndicesCache.java
new file mode 100644
index 0000000..77133d3
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/MarketIndexIndicesCache.java
@@ -0,0 +1,26 @@
+package com.portfolio.redis.model;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+
+import com.am.common.investment.model.equity.MarketIndexIndices;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class MarketIndexIndicesCache {
+ private String key;
+ private String indexSymbol;
+ private String index;
+ private MarketIndexIndices indexIndices;
+ private LocalDateTime timestamp;
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioAnalysis.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioAnalysis.java
new file mode 100644
index 0000000..205d9f7
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioAnalysis.java
@@ -0,0 +1,26 @@
+package com.portfolio.redis.model;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PortfolioAnalysis {
+ private String portfolioId;
+ private Double totalValue;
+ private Double totalGainLoss;
+ private Double totalGainLossPercentage;
+ private List equityHoldings;
+ private LocalDateTime lastUpdated;
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioHoldings.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioHoldings.java
new file mode 100644
index 0000000..dbf5dfd
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioHoldings.java
@@ -0,0 +1,25 @@
+package com.portfolio.redis.model;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PortfolioHoldings {
+ private List equityHoldings;
+ private LocalDateTime lastUpdated;
+ private Double totalValue;
+ private Double totalGainLoss;
+ private Double totalGainLossPercentage;
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioSummaryV1.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioSummaryV1.java
new file mode 100644
index 0000000..1536935
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/PortfolioSummaryV1.java
@@ -0,0 +1,31 @@
+package com.portfolio.redis.model;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import com.am.common.amcommondata.model.enums.BrokerType;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PortfolioSummaryV1 {
+ private Double investmentValue;
+ private Double currentValue;
+ private Double totalGainLoss;
+ private Double totalGainLossPercentage;
+ private Integer totalAssets;
+ private LocalDateTime lastUpdated;
+ private Map brokerPortfolios;
+ private Map> marketCapHoldings;
+ private Map> sectorialHoldings;
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/StockPriceCache.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/StockPriceCache.java
new file mode 100644
index 0000000..142b1a9
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/StockPriceCache.java
@@ -0,0 +1,28 @@
+package com.portfolio.redis.model;
+
+import java.time.Instant;
+
+import com.am.common.investment.model.equity.EquityPrice;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class StockPriceCache {
+ private String symbol;
+ private String isin;
+ private Double price;
+ private Double change;
+ private Double changePercent;
+ private Long volume;
+ private EquityPrice equityPrice;
+ private Instant timestamp;
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/model/TimeInterval.java b/am-common-cache-service/src/main/java/com/portfolio/redis/model/TimeInterval.java
new file mode 100644
index 0000000..9122c2d
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/model/TimeInterval.java
@@ -0,0 +1,34 @@
+package com.portfolio.redis.model;
+
+import java.time.Duration;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum TimeInterval {
+ FIVE_MINUTES(Duration.ofMinutes(5), "5m"),
+ TEN_MINUTES(Duration.ofMinutes(10), "10m"),
+ FIFTEEN_MINUTES(Duration.ofMinutes(15), "15m"),
+ THIRTY_MINUTES(Duration.ofMinutes(30), "30m"),
+ ONE_HOUR(Duration.ofHours(1), "1H"),
+ ONE_DAY(Duration.ofDays(1), "1D"),
+ ONE_WEEK(Duration.ofDays(7), "1W"),
+ ONE_MONTH(Duration.ofDays(30), "1M"),
+ ONE_YEAR(Duration.ofDays(365), "1Y"),
+ OVERALL(null, "all");
+
+ private final Duration duration;
+ private final String code;
+
+ public static TimeInterval fromCode(String code) {
+ if (code == null) return OVERALL;
+
+ for (TimeInterval interval : values()) {
+ if (interval.getCode().equalsIgnoreCase(code)) {
+ return interval;
+ }
+ }
+ throw new IllegalArgumentException("Invalid time interval code: " + code);
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/base/AbstractRedisService.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/base/AbstractRedisService.java
new file mode 100644
index 0000000..50f1214
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/base/AbstractRedisService.java
@@ -0,0 +1,140 @@
+package com.portfolio.redis.service.base;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.core.RedisTemplate;
+
+import com.portfolio.redis.exception.RedisOperationException;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class AbstractRedisService implements RedisOperations {
+
+ protected final RedisTemplate redisTemplate;
+
+ protected abstract String getServiceName();
+ protected abstract Duration getDefaultTtl();
+ protected abstract K buildKey(Object... parts);
+
+ @Override
+ public void set(K key, V value, Duration ttl) {
+ String operation = getServiceName() + ".set";
+
+ try {
+ redisTemplate.opsForValue().set(key, value, ttl);
+ log.debug("[{}] Successfully set value for key: {} with TTL: {}", getServiceName(), key, ttl);
+ } catch (Exception e) {
+ log.error("[{}] Error setting value for key {}: {}", getServiceName(), key, e.getMessage(), e);
+ throw new RedisOperationException("Failed to set value in Redis", e);
+ }
+ }
+
+ @Override
+ public void setBatch(Map entries, Duration ttl) {
+ String operation = getServiceName() + ".setBatch";
+
+ try {
+ redisTemplate.opsForValue().multiSet(entries);
+ entries.keySet().forEach(key ->
+ redisTemplate.expire(key, ttl.getSeconds(), TimeUnit.SECONDS));
+ log.debug("[{}] Successfully set batch of {} entries", getServiceName(), entries.size());
+ } catch (Exception e) {
+ log.error("[{}] Error setting batch entries: {}", getServiceName(), e.getMessage(), e);
+ throw new RedisOperationException("Failed to set batch entries in Redis", e);
+ }
+ }
+
+ @Override
+ public Optional get(K key) {
+ String operation = getServiceName() + ".get";
+
+ try {
+ V value = redisTemplate.opsForValue().get(key);
+
+ if (value != null) {
+ log.debug("[{}] Cache hit for key: {}", getServiceName(), key);
+ return Optional.of(value);
+ } else {
+ log.debug("[{}] Cache miss for key: {}", getServiceName(), key);
+ return Optional.empty();
+ }
+ } catch (Exception e) {
+ log.error("[{}] Error retrieving value for key {}: {}", getServiceName(), key, e.getMessage(), e);
+ throw new RedisOperationException("Failed to get value from Redis", e);
+ }
+ }
+
+ @Override
+ public List getAll(List keys) {
+ String operation = getServiceName() + ".getAll";
+
+ try {
+ return redisTemplate.opsForValue().multiGet(keys);
+ } catch (Exception e) {
+ log.error("[{}] Error retrieving multiple values: {}", getServiceName(), e.getMessage(), e);
+ throw new RedisOperationException("Failed to get multiple values from Redis", e);
+ }
+ }
+
+ @Override
+ public void delete(K key) {
+ String operation = getServiceName() + ".delete";
+
+ try {
+ redisTemplate.delete(key);
+ log.debug("[{}] Successfully deleted key: {}", getServiceName(), key);
+ } catch (Exception e) {
+ log.error("[{}] Error deleting key {}: {}", getServiceName(), key, e.getMessage(), e);
+ throw new RedisOperationException("Failed to delete key from Redis", e);
+ }
+ }
+
+ @Override
+ public void deleteBatch(List keys) {
+ String operation = getServiceName() + ".deleteBatch";
+
+ try {
+ redisTemplate.delete(keys);
+ log.debug("[{}] Successfully deleted {} keys", getServiceName(), keys.size());
+ } catch (Exception e) {
+ log.error("[{}] Error deleting batch of keys: {}", getServiceName(), e.getMessage(), e);
+ throw new RedisOperationException("Failed to delete batch of keys from Redis", e);
+ }
+ }
+
+ @Override
+ public boolean exists(K key) {
+ String operation = getServiceName() + ".exists";
+
+ try {
+ return redisTemplate.hasKey(key);
+ } catch (Exception e) {
+ log.error("[{}] Error checking existence of key {}: {}", getServiceName(), key, e.getMessage(), e);
+ throw new RedisOperationException("Failed to check key existence in Redis", e);
+ }
+ }
+
+ @Override
+ public Duration getTimeToLive(K key) {
+ String operation = getServiceName() + ".getTimeToLive";
+
+ try {
+ Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
+ return ttl != null && ttl > 0 ? Duration.ofSeconds(ttl) : Duration.ZERO;
+ } catch (Exception e) {
+ log.error("[{}] Error getting TTL for key {}: {}", getServiceName(), key, e.getMessage(), e);
+ throw new RedisOperationException("Failed to get TTL from Redis", e);
+ }
+ }
+
+ protected Duration getEffectiveTtl(Duration requestedTtl) {
+ return requestedTtl != null ? requestedTtl : getDefaultTtl();
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/base/RedisOperations.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/base/RedisOperations.java
new file mode 100644
index 0000000..a14ca7e
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/base/RedisOperations.java
@@ -0,0 +1,17 @@
+package com.portfolio.redis.service.base;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public interface RedisOperations {
+ void set(K key, V value, Duration ttl);
+ void setBatch(Map entries, Duration ttl);
+ Optional get(K key);
+ List getAll(List keys);
+ void delete(K key);
+ void deleteBatch(List keys);
+ boolean exists(K key);
+ Duration getTimeToLive(K key);
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/MarketIndexIndicesRedisService.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/MarketIndexIndicesRedisService.java
new file mode 100644
index 0000000..a077853
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/MarketIndexIndicesRedisService.java
@@ -0,0 +1,162 @@
+// package com.portfolio.redis.service.impl;
+
+// import java.time.Duration;
+// import java.time.Instant;
+// import java.time.LocalDateTime;
+// import java.time.ZoneOffset;
+// import java.util.ArrayList;
+// import java.util.List;
+// import java.util.Map;
+// import java.util.Optional;
+// import java.util.concurrent.CompletableFuture;
+// import java.util.stream.Collectors;
+
+// import org.springframework.beans.factory.annotation.Value;
+// import org.springframework.data.redis.core.RedisTemplate;
+// import org.springframework.scheduling.annotation.Async;
+// import org.springframework.stereotype.Service;
+
+// import com.am.common.investment.model.equity.MarketIndexIndices;
+// import com.portfolio.redis.model.MarketIndexIndicesCache;
+// import com.portfolio.redis.service.base.AbstractRedisService;
+// import com.portfolio.redis.util.RedisUtils;
+
+// import lombok.extern.slf4j.Slf4j;
+
+// @Slf4j
+// @Service
+// public class MarketIndexIndicesRedisService extends AbstractRedisService {
+
+// private static final int BATCH_SIZE = 100;
+
+// @Value("${spring.data.redis.market-indices.ttl}")
+// private Integer marketIndicesTtl;
+
+// @Value("${spring.data.redis.market-indices.key-prefix}")
+// private String marketIndicesKeyPrefix;
+
+// @Value("${spring.data.redis.market-indices.historical.ttl}")
+// private Integer marketIndicesHistoricalTtl;
+
+// @Value("${spring.data.redis.market-indices.historical.key-prefix}")
+// private String marketIndicesHistoricalKeyPrefix;
+
+// public MarketIndexIndicesRedisService(
+// RedisTemplate redisTemplate) {
+// super(redisTemplate);
+// }
+
+// @Override
+// protected String getServiceName() {
+// return "MarketIndices";
+// }
+
+// @Override
+// protected Duration getDefaultTtl() {
+// return Duration.ofSeconds(marketIndicesTtl);
+// }
+
+// @Override
+// protected String buildKey(Object... parts) {
+// return RedisUtils.buildKey(marketIndicesKeyPrefix, parts);
+// }
+
+// @Async
+// public CompletableFuture cacheMarketIndexUpdateBatch(List indexUpdates) {
+// return CompletableFuture.runAsync(() -> {
+// if (indexUpdates == null || indexUpdates.isEmpty()) {
+// log.warn("Received empty or null market index updates batch");
+// return;
+// }
+
+// log.info("Starting to process {} market index updates", indexUpdates.size());
+
+// // Process updates in batches
+// for (int i = 0; i < indexUpdates.size(); i += BATCH_SIZE) {
+// int end = Math.min(i + BATCH_SIZE, indexUpdates.size());
+// List batch = indexUpdates.subList(i, end);
+// processBatch(batch);
+// }
+// });
+// }
+
+// private void processBatch(List batch) {
+// try {
+// // Prepare realtime updates
+// Map realtimeUpdates = batch.stream()
+// .map(this::convertToMarketIndexIndicesCache)
+// .collect(Collectors.toMap(
+// index -> buildKey(index.getIndexSymbol()),
+// index -> index
+// ));
+
+// // Prepare historical updates
+// Map historicalUpdates = batch.stream()
+// .map(this::convertToMarketIndexIndicesCache)
+// .collect(Collectors.toMap(
+// index -> buildHistoricalKey(index.getIndexSymbol(), LocalDateTime.now().toInstant(ZoneOffset.UTC)),
+// index -> index
+// ));
+
+// // Cache realtime updates
+// setBatch(realtimeUpdates, getDefaultTtl());
+
+// // Cache historical updates
+// setBatch(historicalUpdates, Duration.ofSeconds(marketIndicesHistoricalTtl));
+// } catch (Exception e) {
+// log.error("Error processing market index update batch: {}", e.getMessage(), e);
+// }
+// }
+
+// public List getHistoricalPrices(String symbol, LocalDateTime startTime, LocalDateTime endTime) {
+// try {
+// String pattern = marketIndicesHistoricalKeyPrefix + symbol + ":*";
+// List historicalPrices = new ArrayList<>();
+
+// redisTemplate.keys(pattern).stream()
+// .map(key -> Optional.ofNullable(get(key).orElse(null)))
+// .filter(Optional::isPresent)
+// .map(Optional::get)
+// .filter(index -> isWithinTimeRange(LocalDateTime.now().toInstant(ZoneOffset.UTC), startTime, endTime))
+// .forEach(historicalPrices::add);
+
+// return historicalPrices;
+// } catch (Exception e) {
+// log.error("Error retrieving historical prices for symbol {}: {}", symbol, e.getMessage(), e);
+// return new ArrayList<>();
+// }
+// }
+
+// public void deleteOldPrices(String symbol, LocalDateTime beforeTime) {
+// try {
+// String pattern = marketIndicesHistoricalKeyPrefix + symbol + ":*";
+// redisTemplate.keys(pattern).stream()
+// .map(key -> Map.entry(key, get(key)))
+// .filter(entry -> entry.getValue().isPresent())
+// .filter(entry -> LocalDateTime.now().isBefore(beforeTime))
+// .forEach(entry -> delete(entry.getKey()));
+// } catch (Exception e) {
+// log.error("Error deleting old market index data for symbol {}: {}", symbol, e.getMessage(), e);
+// }
+// }
+
+// private MarketIndexIndicesCache convertToMarketIndexIndicesCache(MarketIndexIndices index) {
+// return MarketIndexIndicesCache.builder()
+// .key(index.getIndexSymbol())
+// .indexSymbol(index.getIndexSymbol())
+// .index(index.getIndex())
+// .indexIndices(index)
+// .timestamp(index.getTimestamp())
+// .build();
+// }
+
+// private String buildHistoricalKey(String symbol, Instant timestamp) {
+// return RedisUtils.buildKey(marketIndicesHistoricalKeyPrefix, symbol, String.valueOf(timestamp.toEpochMilli()));
+// }
+
+// private boolean isWithinTimeRange(Instant timestamp, LocalDateTime startTime, LocalDateTime endTime) {
+// Instant startInstant = startTime.toInstant(ZoneOffset.UTC);
+// Instant endInstant = endTime.toInstant(ZoneOffset.UTC);
+// return !timestamp.isBefore(startInstant) && !timestamp.isAfter(endInstant);
+// }
+// }
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioAnalysisRedisService.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioAnalysisRedisService.java
new file mode 100644
index 0000000..644991d
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioAnalysisRedisService.java
@@ -0,0 +1,203 @@
+package com.portfolio.redis.service.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import com.portfolio.redis.model.TimeInterval;
+import com.portfolio.redis.model.PortfolioAnalysis;
+import com.portfolio.redis.service.base.AbstractRedisService;
+import com.portfolio.redis.util.RedisUtils;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+public class PortfolioAnalysisRedisService extends AbstractRedisService {
+
+ private static final int BATCH_SIZE = 100;
+
+ @Value("${spring.data.redis.portfolio-analysis.ttl}")
+ private Integer analysisTtl;
+
+ @Value("${spring.data.redis.portfolio-analysis.key-prefix}")
+ private String analysisKeyPrefix;
+
+ @Value("${spring.data.redis.portfolio-analysis.historical.key-prefix}")
+ private String historicalKeyPrefix;
+
+ @Value("${spring.data.redis.portfolio-analysis.historical.ttl}")
+ private Integer historicalTtl;
+
+ public PortfolioAnalysisRedisService(
+ RedisTemplate redisTemplate) {
+ super(redisTemplate);
+ }
+
+ @Override
+ protected String getServiceName() {
+ return "PortfolioAnalysis";
+ }
+
+ @Override
+ protected Duration getDefaultTtl() {
+ return Duration.ofSeconds(analysisTtl);
+ }
+
+ @Override
+ protected String buildKey(Object... parts) {
+ return RedisUtils.buildKey(analysisKeyPrefix, parts);
+ }
+
+ @Async
+ public CompletableFuture cachePortfolioAnalysis(PortfolioAnalysis analysis, String userId, TimeInterval interval) {
+ log.info("Starting async caching of portfolio analysis - User: {}, Interval: {}",
+ userId, interval != null ? interval.getCode() : "null");
+
+ return CompletableFuture.runAsync(() -> {
+ String key = buildKey(userId, interval != null ? interval.getCode() : "all");
+ Duration ttl = getEffectiveTtl(interval);
+
+ set(key, analysis, ttl);
+ log.info("Successfully cached portfolio analysis - User: {}, Key: {}, TTL: {} seconds",
+ userId, key, ttl.getSeconds());
+ });
+ }
+
+ public Optional getLatestAnalysis(String userId, TimeInterval interval) {
+ log.info("Retrieving latest portfolio analysis - User: {}, Interval: {}",
+ userId, interval != null ? interval.getCode() : "null");
+
+ String key = buildKey(userId, interval != null ? interval.getCode() : "all");
+ Optional analysis = get(key);
+
+ if (analysis.isPresent() && interval != null && interval.getDuration() != null) {
+ // Check if the analysis is still fresh (within the interval duration)
+ Instant cutoff = Instant.now().minus(interval.getDuration());
+
+ if (analysis.get().getLastUpdated().toInstant(ZoneOffset.UTC).isAfter(cutoff)) {
+ log.info("Found fresh portfolio analysis in cache - User: {}, Key: {}, LastUpdated: {}",
+ userId, key, analysis.get().getLastUpdated());
+ return analysis;
+ } else {
+ log.info("Found stale portfolio analysis in cache - User: {}, Key: {}, LastUpdated: {}, deleting",
+ userId, key, analysis.get().getLastUpdated());
+ delete(key);
+ return Optional.empty();
+ }
+ }
+
+ return analysis;
+ }
+
+ @Async
+ public CompletableFuture cacheBatchAnalysis(List analyses, String userId, TimeInterval interval) {
+ return CompletableFuture.runAsync(() -> {
+ if (analyses == null || analyses.isEmpty()) {
+ log.warn("Received empty or null analysis batch");
+ return;
+ }
+
+ log.info("Starting to process {} portfolio analyses", analyses.size());
+
+ // Process updates in batches
+ for (int i = 0; i < analyses.size(); i += BATCH_SIZE) {
+ int end = Math.min(i + BATCH_SIZE, analyses.size());
+ List batch = analyses.subList(i, end);
+ processBatch(batch, userId, interval);
+ }
+ });
+ }
+
+ private void processBatch(List batch, String userId, TimeInterval interval) {
+ try {
+ // Prepare realtime updates
+ Map realtimeUpdates = batch.stream()
+ .collect(Collectors.toMap(
+ analysis -> buildKey(userId, interval != null ? interval.getCode() : "all"),
+ analysis -> analysis
+ ));
+
+ // Prepare historical updates
+ Map historicalUpdates = batch.stream()
+ .collect(Collectors.toMap(
+ analysis -> buildHistoricalKey(userId, interval, analysis.getLastUpdated().toInstant(ZoneOffset.UTC)),
+ analysis -> analysis
+ ));
+
+ // Cache realtime updates
+ setBatch(realtimeUpdates, getDefaultTtl());
+
+ // Cache historical updates
+ setBatch(historicalUpdates, Duration.ofSeconds(historicalTtl));
+ } catch (Exception e) {
+ log.error("Error processing analysis batch: {}", e.getMessage(), e);
+ }
+ }
+
+ public List getHistoricalAnalysis(String userId, TimeInterval interval, LocalDateTime startTime, LocalDateTime endTime) {
+ try {
+ String pattern = buildHistoricalPattern(userId, interval);
+ List historicalAnalysis = new ArrayList<>();
+
+ redisTemplate.keys(pattern).stream()
+ .map(key -> get(key))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .filter(analysis -> isWithinTimeRange(analysis.getLastUpdated(), startTime, endTime))
+ .forEach(historicalAnalysis::add);
+
+ return historicalAnalysis;
+ } catch (Exception e) {
+ log.error("Error retrieving historical analysis for user {}: {}", userId, e.getMessage(), e);
+ return new ArrayList<>();
+ }
+ }
+
+ public void deleteOldAnalysis(String userId, TimeInterval interval, LocalDateTime beforeTime) {
+ try {
+ String pattern = buildHistoricalPattern(userId, interval);
+ redisTemplate.keys(pattern).stream()
+ .map(key -> Map.entry(key, get(key)))
+ .filter(entry -> entry.getValue().isPresent())
+ .filter(entry -> entry.getValue().get().getLastUpdated()
+ .isBefore(beforeTime))
+ .forEach(entry -> delete(entry.getKey()));
+ } catch (Exception e) {
+ log.error("Error deleting old analysis for user {}: {}", userId, e.getMessage(), e);
+ }
+ }
+
+ private String buildHistoricalPattern(String userId, TimeInterval interval) {
+ return RedisUtils.buildKey(historicalKeyPrefix, userId, interval != null ? interval.getCode() : "all", "*");
+ }
+
+ private String buildHistoricalKey(String userId, TimeInterval interval, java.time.Instant timestamp) {
+ return RedisUtils.buildKey(historicalKeyPrefix, userId, interval != null ? interval.getCode() : "all", String.valueOf(timestamp.toEpochMilli()));
+ }
+
+ private boolean isWithinTimeRange(LocalDateTime dateTime, LocalDateTime startTime, LocalDateTime endTime) {
+ return !dateTime.isBefore(startTime) && !dateTime.isAfter(endTime);
+ }
+
+ private Duration getEffectiveTtl(TimeInterval interval) {
+ if (interval != null && interval.getDuration() != null) {
+ Duration intervalDuration = interval.getDuration();
+ Duration defaultDuration = getDefaultTtl();
+ return intervalDuration.compareTo(defaultDuration) < 0 ? intervalDuration : defaultDuration;
+ }
+ return getDefaultTtl();
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioHoldingsRedisService.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioHoldingsRedisService.java
new file mode 100644
index 0000000..797845f
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioHoldingsRedisService.java
@@ -0,0 +1,66 @@
+package com.portfolio.redis.service.impl;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import com.portfolio.redis.model.TimeInterval;
+import com.portfolio.redis.model.PortfolioHoldings;
+import com.portfolio.redis.service.base.AbstractRedisService;
+import com.portfolio.redis.util.RedisUtils;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+public class PortfolioHoldingsRedisService extends AbstractRedisService {
+
+ @Value("${spring.data.redis.portfolio-holdings.ttl}")
+ private Integer portfolioHoldingsTtl;
+
+ @Value("${spring.data.redis.portfolio-holdings.key-prefix}")
+ private String portfolioHoldingsKeyPrefix;
+
+ public PortfolioHoldingsRedisService(
+ RedisTemplate redisTemplate) {
+ super(redisTemplate);
+ }
+
+ @Override
+ protected String getServiceName() {
+ return "PortfolioHoldings";
+ }
+
+ @Override
+ protected Duration getDefaultTtl() {
+ return Duration.ofSeconds(portfolioHoldingsTtl);
+ }
+
+ @Override
+ protected String buildKey(Object... parts) {
+ return RedisUtils.buildKey(portfolioHoldingsKeyPrefix, parts);
+ }
+
+ public void cachePortfolioHoldings(PortfolioHoldings holdings, String userId, TimeInterval interval) {
+ String key = buildKey(userId, interval != null ? interval.getCode() : "default");
+ Duration ttl = getEffectiveTtl(interval);
+ set(key, holdings, ttl);
+ }
+
+ public Optional getLatestHoldings(String userId, TimeInterval interval) {
+ String key = buildKey(userId, interval != null ? interval.getCode() : "default");
+ return get(key);
+ }
+
+ private Duration getEffectiveTtl(TimeInterval interval) {
+ if (interval != null && interval.getDuration() != null) {
+ Duration intervalDuration = interval.getDuration();
+ Duration defaultDuration = getDefaultTtl();
+ return intervalDuration.compareTo(defaultDuration) < 0 ? intervalDuration : defaultDuration;
+ }
+ return getDefaultTtl();
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioSummaryRedisService.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioSummaryRedisService.java
new file mode 100644
index 0000000..a9d7233
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/PortfolioSummaryRedisService.java
@@ -0,0 +1,100 @@
+package com.portfolio.redis.service.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import com.portfolio.redis.model.TimeInterval;
+import com.portfolio.redis.model.PortfolioSummaryV1;
+import com.portfolio.redis.service.base.AbstractRedisService;
+import com.portfolio.redis.util.RedisUtils;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+public class PortfolioSummaryRedisService extends AbstractRedisService {
+
+ @Value("${spring.data.redis.portfolio-summary.ttl}")
+ private Integer portfolioSummaryTtl;
+
+ @Value("${spring.data.redis.portfolio-summary.key-prefix}")
+ private String portfolioSummaryKeyPrefix;
+
+ public PortfolioSummaryRedisService(
+ RedisTemplate redisTemplate) {
+ super(redisTemplate);
+ }
+
+ @Override
+ protected String getServiceName() {
+ return "PortfolioSummary";
+ }
+
+ @Override
+ protected Duration getDefaultTtl() {
+ return Duration.ofSeconds(portfolioSummaryTtl);
+ }
+
+ @Override
+ protected String buildKey(Object... parts) {
+ return RedisUtils.buildKey(portfolioSummaryKeyPrefix, parts);
+ }
+
+ @Async
+ public CompletableFuture cachePortfolioSummary(PortfolioSummaryV1 summary, String userId, TimeInterval interval) {
+ log.info("Starting async caching of portfolio summary - User: {}, Interval: {}",
+ userId, interval != null ? interval.getCode() : "null");
+
+ return CompletableFuture.runAsync(() -> {
+ String key = buildKey(userId, interval != null ? interval.getCode() : "all");
+ Duration ttl = getEffectiveTtl(interval);
+
+ set(key, summary, ttl);
+ log.info("Successfully cached portfolio summary - User: {}, Key: {}, TTL: {} seconds",
+ userId, key, ttl.getSeconds());
+ });
+ }
+
+ public Optional getLatestSummary(String userId, TimeInterval interval) {
+ log.info("Retrieving latest portfolio summary - User: {}, Interval: {}",
+ userId, interval != null ? interval.getCode() : "null");
+
+ String key = buildKey(userId, interval != null ? interval.getCode() : "all");
+ Optional summary = get(key);
+
+ if (summary.isPresent() && interval != null && interval.getDuration() != null) {
+ // Check if the summary is still fresh (within the interval duration)
+ Instant cutoff = Instant.now().minus(interval.getDuration());
+
+ if (summary.get().getLastUpdated().toInstant(ZoneOffset.UTC).isAfter(cutoff)) {
+ log.info("Found fresh portfolio summary in cache - User: {}, Key: {}, LastUpdated: {}",
+ userId, key, summary.get().getLastUpdated());
+ return summary;
+ } else {
+ log.info("Found stale portfolio summary in cache - User: {}, Key: {}, LastUpdated: {}, deleting",
+ userId, key, summary.get().getLastUpdated());
+ delete(key);
+ return Optional.empty();
+ }
+ }
+
+ return summary;
+ }
+
+ private Duration getEffectiveTtl(TimeInterval interval) {
+ if (interval != null && interval.getDuration() != null) {
+ Duration intervalDuration = interval.getDuration();
+ Duration defaultDuration = getDefaultTtl();
+ return intervalDuration.compareTo(defaultDuration) < 0 ? intervalDuration : defaultDuration;
+ }
+ return getDefaultTtl();
+ }
+}
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/StockPriceRedisService.java b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/StockPriceRedisService.java
new file mode 100644
index 0000000..20f8755
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/service/impl/StockPriceRedisService.java
@@ -0,0 +1,168 @@
+// package com.portfolio.redis.service.impl;
+
+// import java.time.Duration;
+// import java.time.Instant;
+// import java.time.LocalDateTime;
+// import java.time.ZoneOffset;
+// import java.util.ArrayList;
+// import java.util.List;
+// import java.util.Map;
+// import java.util.Optional;
+// import java.util.concurrent.CompletableFuture;
+// import java.util.stream.Collectors;
+
+// import org.springframework.beans.factory.annotation.Value;
+// import org.springframework.data.redis.core.RedisTemplate;
+// import org.springframework.scheduling.annotation.Async;
+// import org.springframework.stereotype.Service;
+
+// import com.am.common.investment.model.equity.EquityPrice;
+// import com.portfolio.redis.model.StockPriceCache;
+// import com.portfolio.redis.service.base.AbstractRedisService;
+// import com.portfolio.redis.util.RedisUtils;
+
+// import lombok.extern.slf4j.Slf4j;
+
+// @Slf4j
+// @Service
+// public class StockPriceRedisService extends AbstractRedisService {
+
+// private static final int BATCH_SIZE = 100;
+
+// @Value("${spring.data.redis.stock.ttl}")
+// private Integer stockTtl;
+
+// @Value("${spring.data.redis.stock.key-prefix}")
+// private String stockKeyPrefix;
+
+// @Value("${spring.data.redis.stock.historical.key-prefix}")
+// private String historicalKeyPrefix;
+
+// @Value("${spring.data.redis.stock.historical.ttl}")
+// private Integer historicalTtl;
+
+// public StockPriceRedisService(
+// RedisTemplate redisTemplate) {
+// super(redisTemplate);
+// }
+
+// @Override
+// protected String getServiceName() {
+// return "StockPrice";
+// }
+
+// @Override
+// protected Duration getDefaultTtl() {
+// return Duration.ofSeconds(stockTtl);
+// }
+
+// @Override
+// protected String buildKey(Object... parts) {
+// return RedisUtils.buildKey(stockKeyPrefix, parts);
+// }
+
+// @Async
+// public CompletableFuture cacheEquityPriceUpdateBatch(List priceUpdates) {
+// return CompletableFuture.runAsync(() -> {
+// if (priceUpdates == null || priceUpdates.isEmpty()) {
+// log.warn("Received empty or null price updates batch");
+// return;
+// }
+
+// log.info("Starting to process {} price updates", priceUpdates.size());
+
+// // Process updates in batches
+// for (int i = 0; i < priceUpdates.size(); i += BATCH_SIZE) {
+// int end = Math.min(i + BATCH_SIZE, priceUpdates.size());
+// List batch = priceUpdates.subList(i, end);
+// processBatch(batch);
+// }
+// });
+// }
+
+// private void processBatch(List batch) {
+// try {
+// // Prepare realtime updates
+// Map realtimeUpdates = batch.stream()
+// .map(this::convertToStockPriceCache)
+// .collect(Collectors.toMap(
+// price -> buildKey(price.getSymbol()),
+// price -> price
+// ));
+
+// // Prepare historical updates
+// Map historicalUpdates = batch.stream()
+// .map(this::convertToStockPriceCache)
+// .collect(Collectors.toMap(
+// price -> buildHistoricalKey(price.getSymbol(), price.getTimestamp().toInstant(ZoneOffset.UTC)),
+// price -> price
+// ));
+
+// // Cache realtime updates
+// setBatch(realtimeUpdates, getDefaultTtl());
+
+// // Cache historical updates
+// setBatch(historicalUpdates, Duration.ofSeconds(historicalTtl));
+// } catch (Exception e) {
+// log.error("Error processing price update batch: {}", e.getMessage(), e);
+// }
+// }
+
+// public Optional getLatestPrice(String symbol) {
+// return get(buildKey(symbol));
+// }
+
+// public List getHistoricalPrices(String symbol, LocalDateTime startTime, LocalDateTime endTime) {
+// try {
+// String pattern = historicalKeyPrefix + symbol + ":*";
+// List historicalPrices = new ArrayList<>();
+
+// redisTemplate.keys(pattern).stream()
+// .map(key -> get(key))
+// .filter(Optional::isPresent)
+// .map(Optional::get)
+// .filter(price -> isWithinTimeRange(price.getTimestamp(), startTime, endTime))
+// .forEach(historicalPrices::add);
+
+// return historicalPrices;
+// } catch (Exception e) {
+// log.error("Error retrieving historical prices for symbol {}: {}", symbol, e.getMessage(), e);
+// return new ArrayList<>();
+// }
+// }
+
+// public void deleteOldPrices(String symbol, Instant beforeTime) {
+// try {
+// String pattern = historicalKeyPrefix + symbol + ":*";
+// redisTemplate.keys(pattern).stream()
+// .map(key -> Map.entry(key, get(key)))
+// .filter(entry -> entry.getValue().isPresent())
+// .filter(entry -> entry.getValue().get().getTimestamp()
+// .isBefore(beforeTime))
+// .forEach(entry -> delete(entry.getKey()));
+// } catch (Exception e) {
+// log.error("Error deleting old prices for symbol {}: {}", symbol, e.getMessage(), e);
+// }
+// }
+
+// private StockPriceCache convertToStockPriceCache(EquityPrice price) {
+// return StockPriceCache.builder()
+// .symbol(price.getSymbol())
+// .isin(price.getIsin())
+// .price(price.getValue())
+// .change(price.getPriceChange())
+// .changePercent(price.getPriceChangePercent())
+// .volume(price.getVolume())
+// .equityPrice(price)
+// .timestamp(price.getDate().toInstant(ZoneOffset.UTC))
+// .build();
+// }
+
+// private String buildHistoricalKey(String symbol, Instant timestamp) {
+// return RedisUtils.buildKey(historicalKeyPrefix, symbol, String.valueOf(timestamp.toEpochMilli()));
+// }
+
+// private boolean isWithinTimeRange(Instant timestamp, LocalDateTime startTime, LocalDateTime endTime) {
+// return !timestamp.isBefore(startTime.toInstant(ZoneOffset.UTC)) && !timestamp.isAfter(endTime.toInstant(ZoneOffset.UTC));
+// }
+// }
diff --git a/am-common-cache-service/src/main/java/com/portfolio/redis/util/RedisUtils.java b/am-common-cache-service/src/main/java/com/portfolio/redis/util/RedisUtils.java
new file mode 100644
index 0000000..3a0947f
--- /dev/null
+++ b/am-common-cache-service/src/main/java/com/portfolio/redis/util/RedisUtils.java
@@ -0,0 +1,67 @@
+package com.portfolio.redis.util;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RedisUtils {
+
+ public static void batchSet(RedisTemplate redisTemplate, Map keyValueMap, Duration ttl) {
+ try {
+ redisTemplate.opsForValue().multiSet(keyValueMap);
+ keyValueMap.keySet().forEach(key ->
+ redisTemplate.expire(key, ttl.getSeconds(), TimeUnit.SECONDS));
+ log.debug("Successfully batch set {} entries with TTL: {} seconds", keyValueMap.size(), ttl.getSeconds());
+ } catch (Exception e) {
+ log.error("Error during batch set operation: {}", e.getMessage(), e);
+ throw new RuntimeException("Failed to perform batch set operation", e);
+ }
+ }
+
+ public static void deleteByPattern(RedisTemplate redisTemplate, String pattern) {
+ try {
+ Set keys = redisTemplate.keys(pattern);
+ if (keys != null && !keys.isEmpty()) {
+ redisTemplate.delete(keys);
+ log.debug("Successfully deleted {} keys matching pattern: {}", keys.size(), pattern);
+ }
+ } catch (Exception e) {
+ log.error("Error deleting keys with pattern {}: {}", pattern, e.getMessage(), e);
+ throw new RuntimeException("Failed to delete keys by pattern", e);
+ }
+ }
+
+ public static void setWithTtl(RedisTemplate redisTemplate, String key, T value, Duration ttl) {
+ try {
+ redisTemplate.opsForValue().set(key, value, ttl);
+ log.debug("Successfully set key: {} with TTL: {} seconds", key, ttl.getSeconds());
+ } catch (Exception e) {
+ log.error("Error setting key {} with TTL: {}", key, e.getMessage(), e);
+ throw new RuntimeException("Failed to set value with TTL", e);
+ }
+ }
+
+ public static String buildKey(String prefix, Object... parts) {
+ StringBuilder key = new StringBuilder(prefix);
+ for (Object part : parts) {
+ if (part != null) {
+ key.append(String.valueOf(part)).append(":");
+ }
+ }
+ // Remove trailing colon if present
+ if (key.charAt(key.length() - 1) == ':') {
+ key.setLength(key.length() - 1);
+ }
+ return key.toString();
+ }
+}
diff --git a/am-common-cache-service/src/main/resources/application.yml b/am-common-cache-service/src/main/resources/application.yml
new file mode 100644
index 0000000..5d7bca3
--- /dev/null
+++ b/am-common-cache-service/src/main/resources/application.yml
@@ -0,0 +1,25 @@
+spring:
+ data:
+ redis:
+ redisendpoint: localhost:6379
+ portfolio-mover:
+ ttl: 300 # 5 minutes
+ key-prefix: "portfolio:mover:"
+ portfolio-summary:
+ ttl: 300 # 5 minutes
+ key-prefix: "portfolio:summary:"
+ portfolio-holdings:
+ ttl: 300 # 5 minutes
+ key-prefix: "portfolio:holdings:"
+ market-indices:
+ ttl: 300 # 5 minutes
+ key-prefix: "market:indices:"
+ historical:
+ key-prefix: "market:indices:historical:"
+ ttl: 604800 # 7 days
+ stock:
+ ttl: 300 # 5 minutes
+ key-prefix: "stock:price:"
+ historical:
+ key-prefix: "stock:price:historical:"
+ ttl: 604800 # 7 days
diff --git a/am-common-data-app/pom.xml b/am-common-data-app/pom.xml
new file mode 100644
index 0000000..cf514b4
--- /dev/null
+++ b/am-common-data-app/pom.xml
@@ -0,0 +1,138 @@
+
+
+ 4.0.0
+
+
+ com.am.common
+ am-common-data-parent
+ 1.2.10-SNAPSHOT
+
+
+ am-common-data-app
+ AM Common Data Application
+
+
+
+
+ com.am.common
+ am-common-data-model
+ ${project.version}
+
+
+
+ com.am.common
+ am-common-data-service
+ ${project.version}
+
+
+ com.am.common
+ am-common-data-mongo
+ ${project.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+ ${spring-boot.version}
+
+
+
+
+ com.h2database
+ h2
+ 2.1.214
+ runtime
+
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+ org.testcontainers
+ mongodb
+ 1.19.5
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ **/*Test.java
+ **/*Tests.java
+ **/*ContainerTest.java
+
+ 1
+ false
+ -Xmx1024m
+
+
+
+
+
diff --git a/am-common-data-app/src/main/java/com/am/common/amcommondata/AmCommonDataApplication.java b/am-common-data-app/src/main/java/com/am/common/amcommondata/AmCommonDataApplication.java
new file mode 100644
index 0000000..a4b7e3b
--- /dev/null
+++ b/am-common-data-app/src/main/java/com/am/common/amcommondata/AmCommonDataApplication.java
@@ -0,0 +1,28 @@
+package com.am.common.amcommondata;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.ComponentScans;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@ComponentScans({
+ @ComponentScan("com.am.common.amcommondata"),
+ @ComponentScan("com.am.common.amcommondata.service"),
+ @ComponentScan("com.portfolio")
+})
+@EnableJpaRepositories(basePackages = {
+ "com.am.common.amcommondata.repository.asset",
+ "com.am.common.amcommondata.repository.security"
+})
+@EntityScan(basePackages = {
+ "com.am.common.amcommondata.domain",
+ "com.am.common.amcommondata.domain.asset"
+})
+@SpringBootApplication
+public class AmCommonDataApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(AmCommonDataApplication.class, args);
+ }
+}
diff --git a/am-common-data-app/src/main/java/com/am/common/amcommondata/controller/PortfolioController.java b/am-common-data-app/src/main/java/com/am/common/amcommondata/controller/PortfolioController.java
new file mode 100644
index 0000000..4c8fa47
--- /dev/null
+++ b/am-common-data-app/src/main/java/com/am/common/amcommondata/controller/PortfolioController.java
@@ -0,0 +1,43 @@
+// package com.am.common.amcommondata.controller;
+
+// import com.am.common.amcommondata.model.PortfolioModel;
+// import com.am.common.amcommondata.service.PortfolioService;
+// import lombok.RequiredArgsConstructor;
+// import org.springframework.http.ResponseEntity;
+// import org.springframework.web.bind.annotation.*;
+
+// import java.util.List;
+// import java.util.UUID;
+
+// @RestController
+// @RequestMapping("/portfolios")
+// @RequiredArgsConstructor
+// public class PortfolioController {
+// private final PortfolioService portfolioService;
+
+// @GetMapping
+// public ResponseEntity> getAllPortfolios() {
+// return ResponseEntity.ok(portfolioService.getAllPortfolios());
+// }
+
+// @GetMapping("/{id}")
+// public ResponseEntity getPortfolioById(@PathVariable UUID id) {
+// return ResponseEntity.ok(portfolioService.getPortfolio(id));
+// }
+
+// @PostMapping
+// public ResponseEntity createPortfolio(@RequestBody PortfolioModel portfolio) {
+// return ResponseEntity.ok(portfolioService.createPortfolio(portfolio));
+// }
+
+// @PutMapping("/{id}")
+// public ResponseEntity updatePortfolio(@PathVariable UUID id, @RequestBody PortfolioModel portfolio) {
+// return ResponseEntity.ok(portfolioService.updatePortfolio(id, portfolio));
+// }
+
+// @DeleteMapping("/{id}")
+// public ResponseEntity deletePortfolio(@PathVariable UUID id) {
+// portfolioService.deletePortfolio(id);
+// return ResponseEntity.noContent().build();
+// }
+// }
diff --git a/am-common-data-app/src/main/java/com/am/common/amcommondata/exception/GlobalExceptionHandler.java b/am-common-data-app/src/main/java/com/am/common/amcommondata/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..ad81a50
--- /dev/null
+++ b/am-common-data-app/src/main/java/com/am/common/amcommondata/exception/GlobalExceptionHandler.java
@@ -0,0 +1,35 @@
+package com.am.common.amcommondata.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.WebRequest;
+
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(RuntimeException.class)
+ public ResponseEntity