JdbcTemplate, MongoDB and Redis with Spring Boot
Not every problem needs JPA. Spring Boot supports plain SQL with JdbcTemplate, document data with MongoDB and fast key-value access with Redis, all with the same repository style and auto-configuration.
Choose the right tool
| Need | Use | Why |
|---|---|---|
| Complex SQL, reports, bulk operations, full control | JdbcTemplate / Spring Data JDBC | No entity state, no lazy loading surprises, faster for reads and batches |
| Rich domain model with relations | Spring Data JPA | Mapping and productivity |
| Flexible documents without fixed schema | MongoDB | Nested JSON documents, easy schema changes |
| Cache, sessions, counters, rate limits, leaderboards | Redis | In-memory speed, TTL, atomic operations |
JdbcTemplate
@Repository
public class OrderDao {
private final JdbcTemplate jdbc;
private final NamedParameterJdbcTemplate named;
public OrderDao(JdbcTemplate jdbc, NamedParameterJdbcTemplate named) {
this.jdbc = jdbc;
this.named = named;
}
public record OrderRow(long id, String customer, BigDecimal total) { }
private static final RowMapper<OrderRow> MAPPER =
(rs, i) -> new OrderRow(rs.getLong("id"), rs.getString("customer"), rs.getBigDecimal("total"));
public List<OrderRow> findByCustomer(String customer) {
return named.query("select id, customer, total from orders where customer = :c order by id",
Map.of("c", customer), MAPPER);
}
public Optional<OrderRow> findById(long id) {
return jdbc.query("select id, customer, total from orders where id = ?", MAPPER, id).stream().findFirst();
}
public long insert(String customer, BigDecimal total) {
KeyHolder keys = new GeneratedKeyHolder();
jdbc.update(con -> {
PreparedStatement ps = con.prepareStatement(
"insert into orders(customer, total) values (?, ?)", new String[] { "id" });
ps.setString(1, customer);
ps.setBigDecimal(2, total);
return ps;
}, keys);
return keys.getKey().longValue();
}
public int[] batchInsert(List<OrderRow> rows) {
return jdbc.batchUpdate("insert into orders(customer, total) values (?, ?)", new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, rows.get(i).customer());
ps.setBigDecimal(2, rows.get(i).total());
}
public int getBatchSize() { return rows.size(); }
});
}
}Spring translates SQLException into unchecked DataAccessException subclasses (for example DuplicateKeyException), so your code is independent of the database vendor.
MongoDB
spring.data.mongodb.uri=mongodb://localhost:27017/shop@Document(collection = "reviews")
public class Review {
@Id private String id;
@Indexed private String productId;
private int rating;
private String text;
private List<String> tags = new ArrayList<>();
private Instant createdAt = Instant.now();
// getters and setters
}
public interface ReviewRepository extends MongoRepository<Review, String> {
List<Review> findByProductIdOrderByCreatedAtDesc(String productId);
List<Review> findByRatingGreaterThanEqual(int rating);
@Query("{ 'tags': ?0 }")
List<Review> findByTag(String tag);
}
@Service
public class ReviewStats {
private final MongoTemplate mongo;
public ReviewStats(MongoTemplate mongo) { this.mongo = mongo; }
public List<Document> averageRatingPerProduct() {
Aggregation agg = Aggregation.newAggregation(
Aggregation.group("productId").avg("rating").as("avgRating").count().as("reviews"),
Aggregation.sort(Sort.Direction.DESC, "avgRating"));
return mongo.aggregate(agg, "reviews", Document.class).getMappedResults();
}
}Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.cache.type=redis
spring.cache.redis.time-to-live=10m@Service
public class OtpService {
private final StringRedisTemplate redis;
public OtpService(StringRedisTemplate redis) { this.redis = redis; }
public void save(String phone, String otp) {
redis.opsForValue().set("otp:" + phone, otp, Duration.ofMinutes(5)); // expires automatically
}
public boolean verify(String phone, String otp) {
String stored = redis.opsForValue().get("otp:" + phone);
return otp.equals(stored) && Boolean.TRUE.equals(redis.delete("otp:" + phone));
}
public long hit(String userId) { // simple rate limit counter
String key = "rate:" + userId;
Long count = redis.opsForValue().increment(key);
if (count != null && count == 1) redis.expire(key, Duration.ofMinutes(1));
return count == null ? 0 : count;
}
}- Redis data types: String, Hash, List, Set, Sorted Set. Use RedisTemplate opsForHash, opsForList, opsForZSet.
- Choose a serializer on purpose. The default JDK serialization is unreadable and fragile. Use StringRedisSerializer for keys and a JSON serializer for values.
- Always set an expiry (TTL) on cache and session keys, or memory will grow without limit.
Common mistake: Using Redis as the only store for important business data without persistence and backup planning. Redis is memory first. Treat it as cache and fast state unless you have configured persistence (AOF or RDB) and replication.
Interview questions
JdbcTemplate or JPA?
JPA for domain models with many relations and CRUD. JdbcTemplate for reports, batch jobs, bulk changes and when you want exact SQL. Many projects use both.
How does MongoDB differ from a relational database?
It stores JSON-like documents in collections without a fixed schema, and you embed related data in one document instead of joining tables.
What is the difference between Redis as cache and as a database?
As cache the data can be rebuilt and expires. As a database you must configure persistence and accept memory cost.