Back to Journal
System Design

Complete Guide to Database Sharding with Java

A comprehensive guide to implementing Database Sharding using Java, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 12 min read

Java's mature database connectivity ecosystem, connection pool libraries, and enterprise deployment patterns make it a common choice for sharded database architectures. This guide covers implementing database sharding in Java using Spring Boot, HikariCP for connection pooling, and a custom routing layer that integrates cleanly with Spring's transaction management.

Architecture with Spring Boot

The sharding layer integrates into Spring Boot as a custom DataSource that routes queries based on a shard key stored in a thread-local context.

java
1// Shard context holder — thread-local shard key
2public class ShardContext {
3 private static final ThreadLocal<String> currentShard = new ThreadLocal<>();
4 
5 public static void setShardKey(String key) {
6 currentShard.set(key);
7 }
8 
9 public static String getShardKey() {
10 return currentShard.get();
11 }
12 
13 public static void clear() {
14 currentShard.remove();
15 }
16}
17 

Shard Router

java
1@Component
2public class ConsistentHashRouter {
3 private final TreeMap<Long, String> ring = new TreeMap<>();
4 private final int virtualNodes;
5 
6 public ConsistentHashRouter(@Value("${sharding.virtual-nodes:150}") int virtualNodes) {
7 this.virtualNodes = virtualNodes;
8 }
9 
10 public void addShard(String shardId) {
11 for (int i = 0; i < virtualNodes; i++) {
12 long hash = hash(shardId + ":" + i);
13 ring.put(hash, shardId);
14 }
15 }
16 
17 public String route(String key) {
18 if (ring.isEmpty()) throw new IllegalStateException("No shards configured");
19 long hash = hash(key);
20 Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
21 return (entry != null) ? entry.getValue() : ring.firstEntry().getValue();
22 }
23 
24 private long hash(String key) {
25 return Hashing.murmur3_128().hashString(key, StandardCharsets.UTF_8).asLong();
26 }
27}
28 

Dynamic DataSource Routing

Spring's AbstractRoutingDataSource provides the integration point for transparent shard routing.

java
1public class ShardRoutingDataSource extends AbstractRoutingDataSource {
2 
3 private final ConsistentHashRouter router;
4 
5 public ShardRoutingDataSource(ConsistentHashRouter router, Map<String, DataSource> shardDataSources) {
6 this.router = router;
7 Map<Object, Object> targetDataSources = new HashMap<>(shardDataSources);
8 setTargetDataSources(targetDataSources);
9 setDefaultTargetDataSource(shardDataSources.values().iterator().next());
10 afterPropertiesSet();
11 }
12 
13 @Override
14 protected Object determineCurrentLookupKey() {
15 String shardKey = ShardContext.getShardKey();
16 if (shardKey == null) {
17 return "default";
18 }
19 return router.route(shardKey);
20 }
21}
22 
23@Configuration
24public class ShardingConfig {
25 
26 @Bean
27 public DataSource dataSource(
28 ConsistentHashRouter router,
29 @Value("${sharding.shard-count}") int shardCount,
30 Environment env
31 ) {
32 Map<String, DataSource> shardDataSources = new HashMap<>();
33 
34 for (int i = 0; i < shardCount; i++) {
35 String shardId = "shard-" + i;
36 String prefix = "sharding.shards." + shardId;
37 
38 HikariConfig config = new HikariConfig();
39 config.setJdbcUrl(env.getProperty(prefix + ".url"));
40 config.setUsername(env.getProperty(prefix + ".username"));
41 config.setPassword(env.getProperty(prefix + ".password"));
42 config.setMaximumPoolSize(
43 Integer.parseInt(env.getProperty(prefix + ".max-pool-size", "20"))
44 );
45 config.setMinimumIdle(5);
46 config.setConnectionTimeout(5000);
47 config.setIdleTimeout(300000);
48 config.setMaxLifetime(1800000);
49 config.setPoolName("shard-" + i + "-pool");
50 
51 DataSource ds = new HikariDataSource(config);
52 shardDataSources.put(shardId, ds);
53 router.addShard(shardId);
54 }
55 
56 return new ShardRoutingDataSource(router, shardDataSources);
57 }
58}
59 

Repository with Shard-Aware Queries

java
1@Repository
2public class OrderRepository {
3 
4 private final JdbcTemplate jdbc;
5 
6 public OrderRepository(JdbcTemplate jdbc) {
7 this.jdbc = jdbc;
8 }
9 
10 @Transactional
11 public void create(Order order) {
12 ShardContext.setShardKey(order.getTenantId());
13 try {
14 jdbc.update("""
15 INSERT INTO orders (tenant_id, order_id, customer_id, status, total_cents, currency, created_at)
16 VALUES (?, ?, ?, ?, ?, ?, ?)
17 """,
18 order.getTenantId(), order.getOrderId(), order.getCustomerId(),
19 order.getStatus(), order.getTotalCents(), order.getCurrency(), order.getCreatedAt()
20 );
21 } finally {
22 ShardContext.clear();
23 }
24 }
25 
26 public Optional<Order> findById(String tenantId, String orderId) {
27 ShardContext.setShardKey(tenantId);
28 try {
29 return jdbc.query("""
30 SELECT tenant_id, order_id, customer_id, status, total_cents, currency, created_at
31 FROM orders WHERE tenant_id = ? AND order_id = ?
32 """,
33 this::mapRow, tenantId, orderId
34 ).stream().findFirst();
35 } finally {
36 ShardContext.clear();
37 }
38 }
39 
40 public List<Order> findByTenant(String tenantId, int limit, int offset) {
41 ShardContext.setShardKey(tenantId);
42 try {
43 return jdbc.query("""
44 SELECT tenant_id, order_id, customer_id, status, total_cents, currency, created_at
45 FROM orders WHERE tenant_id = ?
46 ORDER BY created_at DESC LIMIT ? OFFSET ?
47 """,
48 this::mapRow, tenantId, limit, offset
49 );
50 } finally {
51 ShardContext.clear();
52 }
53 }
54 
55 private Order mapRow(ResultSet rs, int rowNum) throws SQLException {
56 return new Order(
57 rs.getString("tenant_id"),
58 rs.getString("order_id"),
59 rs.getString("customer_id"),
60 rs.getString("status"),
61 rs.getLong("total_cents"),
62 rs.getString("currency"),
63 rs.getTimestamp("created_at").toInstant()
64 );
65 }
66}
67 

Need a second opinion on your system design architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Cross-Shard Query Executor

java
1@Component
2public class CrossShardExecutor {
3 
4 private final Map<String, DataSource> shardDataSources;
5 private final ExecutorService executor;
6 
7 public CrossShardExecutor(
8 @Qualifier("shardDataSourceMap") Map<String, DataSource> shardDataSources,
9 @Value("${sharding.scatter-parallelism:8}") int parallelism
10 ) {
11 this.shardDataSources = shardDataSources;
12 this.executor = Executors.newFixedThreadPool(parallelism);
13 }
14 
15 public <T> List<T> scatterGather(
16 String sql,
17 Object[] params,
18 RowMapper<T> mapper,
19 Duration timeout
20 ) {
21 List<CompletableFuture<List<T>>> futures = shardDataSources.entrySet().stream()
22 .map(entry -> CompletableFuture.supplyAsync(() -> {
23 JdbcTemplate shardJdbc = new JdbcTemplate(entry.getValue());
24 return shardJdbc.query(sql, mapper, params);
25 }, executor))
26 .toList();
27 
28 return futures.stream()
29 .map(f -> {
30 try {
31 return f.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
32 } catch (Exception e) {
33 return Collections.<T>emptyList();
34 }
35 })
36 .flatMap(List::stream)
37 .toList();
38 }
39 
40 public Map<String, Long> aggregateCount(String sql, Object[] params) {
41 Map<String, Long> results = new ConcurrentHashMap<>();
42 
43 List<CompletableFuture<Void>> futures = shardDataSources.entrySet().stream()
44 .map(entry -> CompletableFuture.runAsync(() -> {
45 JdbcTemplate shardJdbc = new JdbcTemplate(entry.getValue());
46 Long count = shardJdbc.queryForObject(sql, Long.class, params);
47 results.put(entry.getKey(), count != null ? count : 0L);
48 }, executor))
49 .toList();
50 
51 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
52 return results;
53 }
54}
55 

AOP-Based Shard Routing

Use annotations to declaratively set the shard context instead of manual ShardContext management.

java
1@Target(ElementType.METHOD)
2@Retention(RetentionPolicy.RUNTIME)
3public @interface Sharded {
4 String shardKeyParam() default "tenantId";
5}
6 
7@Aspect
8@Component
9public class ShardRoutingAspect {
10 
11 @Around("@annotation(sharded)")
12 public Object routeToShard(ProceedingJoinPoint joinPoint, Sharded sharded) throws Throwable {
13 Object[] args = joinPoint.getArgs();
14 MethodSignature signature = (MethodSignature) joinPoint.getSignature();
15 String[] paramNames = signature.getParameterNames();
16 
17 String shardKey = null;
18 for (int i = 0; i < paramNames.length; i++) {
19 if (paramNames[i].equals(sharded.shardKeyParam())) {
20 shardKey = args[i].toString();
21 break;
22 }
23 }
24 
25 if (shardKey == null) {
26 throw new IllegalArgumentException("Shard key parameter not found: " + sharded.shardKeyParam());
27 }
28 
29 ShardContext.setShardKey(shardKey);
30 try {
31 return joinPoint.proceed();
32 } finally {
33 ShardContext.clear();
34 }
35 }
36}
37 
38// Usage
39@Service
40public class OrderService {
41 
42 @Sharded(shardKeyParam = "tenantId")
43 public Order getOrder(String tenantId, String orderId) {
44 return orderRepository.findById(tenantId, orderId)
45 .orElseThrow(() -> new OrderNotFoundException(orderId));
46 }
47}
48 

Shard Health Monitoring

java
1@Component
2public class ShardHealthIndicator implements HealthIndicator {
3 
4 private final Map<String, DataSource> shardDataSources;
5 
6 @Override
7 public Health health() {
8 Map<String, Object> details = new HashMap<>();
9 boolean allHealthy = true;
10 
11 for (var entry : shardDataSources.entrySet()) {
12 try {
13 if (entry.getValue() instanceof HikariDataSource hikari) {
14 HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
15 details.put(entry.getKey(), Map.of(
16 "active", pool.getActiveConnections(),
17 "idle", pool.getIdleConnections(),
18 "total", pool.getTotalConnections(),
19 "waiting", pool.getThreadsAwaitingConnection()
20 ));
21 }
22 
23 try (Connection conn = entry.getValue().getConnection()) {
24 conn.isValid(2);
25 }
26 } catch (Exception e) {
27 details.put(entry.getKey(), "UNHEALTHY: " + e.getMessage());
28 allHealthy = false;
29 }
30 }
31 
32 return allHealthy
33 ? Health.up().withDetails(details).build()
34 : Health.down().withDetails(details).build();
35 }
36}
37 

Conclusion

Java's Spring Boot ecosystem provides excellent building blocks for database sharding: AbstractRoutingDataSource for transparent shard selection, HikariCP for high-performance connection pooling, and AOP for declarative shard routing. The combination of thread-local shard context with Spring's transaction management ensures that all operations within a request scope target the correct shard without leaking cross-request state.

For most Java applications, this Spring-integrated approach is the most maintainable path to sharding. The @Sharded annotation pattern keeps business logic clean while the routing infrastructure handles the complexity.

FAQ

Need expert help?

Building with system design?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026