Skip to main content

NoSQL Trade-offs — Practical Demo

Hands-on Spring Boot examples for NoSQL Trade-offs. Covers Redis caching patterns and MongoDB CRUD with Spring Data.

Prerequisites

Understand SQL Fundamentals and Transactions & ACID first — knowing what relational databases provide is essential to understanding what you're trading away with NoSQL.


Part A: Redis — Caching with Spring Data Redis

Setup

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
application.yml
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:} # blank for local dev
timeout: 2000ms # ← connection timeout; fail fast if Redis is down
cache:
type: redis
redis:
time-to-live: 600000 # 10 minutes default TTL for all caches

Example 1: @Cacheable — Simple Cache

ProductService.java
@Service
public class ProductService {

@Cacheable(value = "products", key = "#id") // ← cache result by product ID
public ProductDto getProduct(Long id) {
log.info("Cache miss — fetching product {} from DB", id);
return productRepository.findById(id)
.map(ProductDto::from)
.orElseThrow(() -> new EntityNotFoundException("Product " + id));
}

@CacheEvict(value = "products", key = "#dto.id") // ← remove stale entry on update
public ProductDto updateProduct(ProductDto dto) {
Product saved = productRepository.save(ProductDto.toEntity(dto));
return ProductDto.from(saved);
}

@CacheEvict(value = "products", allEntries = true) // ← flush entire cache on bulk refresh
public void refreshAll() { }
}

Cache behavior:

  1. First call: cache miss → DB query → result stored in Redis with a 10-minute TTL
  2. Subsequent calls (within 10 min): cache hit → Redis lookup, no DB query
  3. After updateProduct: the entry is evicted, next read is a fresh DB hit

Example 2: RedisTemplate — Manual Cache with Custom TTL

For fine-grained control over TTL and data structures:

SessionCacheService.java
@Service
public class SessionCacheService {

private final RedisTemplate<String, String> redisTemplate;

public SessionCacheService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}

public void storeSession(String sessionId, String userId) {
String key = "session:" + sessionId;
redisTemplate.opsForValue().set(
key,
userId,
30, TimeUnit.MINUTES // ← per-entry TTL
);
}

public Optional<String> getUserIdFromSession(String sessionId) {
String val = redisTemplate.opsForValue().get("session:" + sessionId);
return Optional.ofNullable(val);
}

public void invalidateSession(String sessionId) {
redisTemplate.delete("session:" + sessionId);
}
}

Example 3: Redis Sorted Set — Real-Time Leaderboard

LeaderboardService.java
@Service
public class LeaderboardService {

private static final String LEADERBOARD_KEY = "leaderboard:global";
private final RedisTemplate<String, String> redisTemplate;

// Update a user's score after completing an action
public void addScore(String userId, double points) {
redisTemplate.opsForZSet().incrementScore(
LEADERBOARD_KEY, userId, points // ← ZINCRBY: atomic, O(log N)
);
}

// Get top 10 users with their scores (highest first)
public List<Map.Entry<String, Double>> getTop10() {
Set<ZSetOperations.TypedTuple<String>> topUsers =
redisTemplate.opsForZSet().reverseRangeWithScores(
LEADERBOARD_KEY, 0, 9 // ← ZREVRANGEBYSCORE with WITHSCORES
);

return topUsers.stream()
.map(t -> Map.entry(t.getValue(), t.getScore()))
.collect(Collectors.toList());
}
}

Example 4: Rate Limiting with Redis + Lua Script

RateLimiterService.java
@Service
public class RateLimiterService {

private final StringRedisTemplate redisTemplate;

// Sliding-window rate limit: max N requests per window
public boolean isAllowed(String clientId, int maxRequests, int windowSeconds) {
String key = "rate:" + clientId;

// Atomic Lua script: increment counter, set TTL on first request
String script = """
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1]) -- set TTL on first request
end
return current
""";

Long count = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
List.of(key),
String.valueOf(windowSeconds)
);

return count != null && count <= maxRequests;
}
}

Part B: MongoDB — Document CRUD with Spring Data

Setup

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
application.yml
spring:
data:
mongodb:
uri: mongodb://${MONGO_HOST:localhost}:27017/${MONGO_DB:myapp}

Example 5: MongoDB Document Entity — Product Catalog

Product.java
@Document(collection = "products")   // ← maps to MongoDB collection
@CompoundIndex(def = "{'category': 1, 'price': -1}", name = "idx_cat_price")
public class Product {

@Id
private String id; // ← MongoDB uses String ObjectId by default

private String name;
private String category;
private BigDecimal price;

private List<String> tags; // ← arrays are first-class in MongoDB

private Map<String, Object> specs; // ← flexible schema: varies per product type

@CreatedDate
private Instant createdAt;
}
ProductRepository.java
public interface ProductRepository extends MongoRepository<Product, String> {

// Spring Data derives query from method name
List<Product> findByCategoryAndPriceLessThan(String category, BigDecimal maxPrice);

List<Product> findByTagsContaining(String tag);

@Query("{ 'specs.color': ?0 }") // ← custom MongoDB query expression
List<Product> findBySpecsColor(String color);
}

Example 6: MongoDB Aggregation Pipeline

ProductAnalyticsService.java
@Service
public class ProductAnalyticsService {

private final MongoTemplate mongoTemplate;

// Average price per category, sorted highest first
public List<CategoryPriceSummary> avgPriceByCategory() {
Aggregation agg = Aggregation.newAggregation(
Aggregation.group("category") // $group by category
.avg("price").as("avgPrice")
.count().as("productCount"),
Aggregation.sort(Sort.Direction.DESC, "avgPrice"),
Aggregation.project("avgPrice", "productCount")
.and("_id").as("category")
);

AggregationResults<CategoryPriceSummary> results =
mongoTemplate.aggregate(agg, "products", CategoryPriceSummary.class);

return results.getMappedResults();
}

public record CategoryPriceSummary(String category, BigDecimal avgPrice, int productCount) {}
}

Example 7: MongoDB — Embedded vs Referenced Documents

Order.java (embedded line items)
@Document(collection = "orders")
public class Order {

@Id
private String id;

private String userId; // ← reference to User (by ID, not embedded)

private List<LineItem> items; // ← embedded sub-documents (denormalized)

private BigDecimal totalAmount;
private String status;
private Instant createdAt;

// Embedded document — no separate collection needed
public record LineItem(String productId, String name, int quantity, BigDecimal unitPrice) {}
}

Design rule: Embed data that is always read/written together with the parent (line items with their order). Reference data that is shared or accessed independently (user referenced by ID — user data shouldn't be duplicated in every order).


Example 8: Integration Test with Flapdoodle (Embedded MongoDB)

pom.xml (test scope)
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring3x</artifactId>
<scope>test</scope>
</dependency>
ProductRepositoryTest.java
@DataMongoTest                    // ← loads only MongoDB layers, not full Spring context
class ProductRepositoryTest {

@Autowired
private ProductRepository repository;

@Test
void shouldFindByCategory() {
repository.save(new Product(null, "Laptop Pro", "electronics",
new BigDecimal("1299.99"), List.of("laptop", "ultrabook"), Map.of(), null));
repository.save(new Product(null, "Phone X", "electronics",
new BigDecimal("899.99"), List.of("phone"), Map.of(), null));

List<Product> electronics = repository.findByCategoryAndPriceLessThan(
"electronics", new BigDecimal("1000.00"));

assertThat(electronics).hasSize(1);
assertThat(electronics.get(0).getName()).isEqualTo("Phone X");
}
}

Summary

TechnologyUse ForSpring Boot Starter
Redis @CacheableTransparent method-level cachingspring-boot-starter-cache + spring-boot-starter-data-redis
Redis RedisTemplateCustom TTLs, data structures, Lua scriptsspring-boot-starter-data-redis
MongoDB MongoRepositoryDocument CRUD with derived queriesspring-boot-starter-data-mongodb
MongoDB MongoTemplateAggregation pipelines, complex queriespart of spring-boot-starter-data-mongodb

Return to the full note: NoSQL Trade-offs