Mastering Spring Boot Data Access: From ORM and Caching to Search and Distributed Consistency
This extensive guide redesigns Spring Boot data‑access for high‑traffic e‑commerce, explaining why traditional JPA‑Redis‑Elasticsearch thinking fails, then detailing a multimodal architecture that assigns strong‑consistency, hot‑read, document, and search responsibilities to MySQL, Redis, MongoDB and Elasticsearch, with production‑grade code, CDC pipelines, distributed‑transaction patterns, caching strategies, observability, and cloud‑native deployment.
Why Refactor Spring Boot Data Access
Many teams treat Spring Boot data access as "JPA for CRUD, Redis for cache, ES for search". In medium‑to‑large systems this leads to three problems:
Components are used as tools instead of carriers of capability boundaries.
Only functional correctness is considered; consistency, throughput, latency, capacity and recoverability are ignored.
Code can run, but there is no closed‑loop among data flow, control flow, transaction flow and synchronization flow at the architecture level.
A mature data‑access system builds a multimodal architecture:
MySQL/PostgreSQL for strong‑consistent core state.
Redis for hot reads, idempotent control, rate‑limiting and short‑lived state.
MongoDB for rapidly changing schema and document‑oriented aggregation.
Elasticsearch for full‑text search, aggregation and ranking.
Kafka/CDC for asynchronous decoupling and cross‑store synchronization.
We use a typical e‑commerce mid‑platform as a running example.
Business Background and Architecture Goals
Business Scenario
Product Center: tens of millions of SKUs, dynamic attributes, complex filtering, search recommendation.
Order Center: extremely high write intensity, payment chain requires strong consistency.
Inventory Center: deduction, freezing and replenishment under extreme concurrency.
User Center: hotspot account reads, many short‑lived states.
Search Center: millisecond‑level retrieval, suggestions, aggregation filters.
Key Metrics
Dimension Target
--------------------------- -----------------
Peak order write 5w‑10w TPS
Hot read requests 50w‑100w QPS
Search latency (P99) < 80ms
Order query latency (P99) < 20ms
Core‑link consistency Strong consistency
Non‑core sync Seconds‑level eventual consistency
Deployment Kubernetes + observability stackSelection Principle: Define Data Responsibility First, Then Choose Access Technology
Selection Logic Table (simplified)
Data Type Access Feature Recommended Tech Reason
----------------------- ------------------------------ --------------------------- -----------------------------------
Order, payment, balance Strong consistency, transaction MySQL + JPA / MyBatis ACID, mature indexes, strong txn support
Product detail hotspot Read‑heavy, obvious hotspot Redis + Caffeine Reduce RT, cut peaks, protect DB
Product attributes Fast schema change, nested data MongoDB Document model fits aggregation data
Search, filter, suggest Fuzzy match, aggregation, sort Elasticsearch Inverted index and analysis capabilities
Cross‑store sync Asynchronous, eventual consistency Kafka + CDC Decouple, avoid double‑writeCommon Pitfalls
Pitfall 1: Believing JPA Can Do Everything – JPA is strong but should not be forced to handle complex reports, massive batch writes or heavy SQL optimisation. It fits aggregate roots with clear consistency boundaries.
Pitfall 2: Treating Redis Only as Cache – In production Redis also handles hotspot caching, idempotent flags, distributed locks, delayed queues, counters, rate limiting and short‑term sessions. Ignoring these roles underestimates its impact on system stability.
Pitfall 3: Synchronous Double‑Write Between DB and ES – Writing to both the relational DB and Elasticsearch in the same transaction leads to inconsistency, dirty indexes and retry chaos. The recommended pattern is
transaction DB → outbox → Kafka → index consumer → Elasticsearch.
Core Technology 1 – JPA Fundamentals and Production Practices
What JPA Really Provides
Bind domain objects to a persistence context, forming a consistent in‑memory model.
Leverage dirty checking and transaction boundaries to reduce boiler‑plate update code.
Model business state transitions with aggregate roots instead of scripting services.
Essential Mechanisms
Persistence Context – Within a transaction the EntityManager maintains a single managed instance per primary key, enabling unified change detection.
Dirty Checking – Before commit Hibernate compares entity snapshots and generates SQL. Large object graphs or long transactions increase CPU and memory overhead.
Lazy Loading – Avoids unnecessary queries but accessing associations at the wrong layer causes classic N+1 problems.
First‑Level & Second‑Level Cache – First‑level cache is transaction‑scoped and mandatory. Second‑level cache is shared across transactions and suitable for low‑frequency reference data, not for frequently updated core transaction data.
Production‑Grade Configuration Baseline
spring:
datasource:
url: jdbc:mysql://mysql-primary:3306/order_db?useUnicode=true&characterEncoding=utf8mb4&rewriteBatchedStatements=true&useSSL=false
username: ${DB_USER}
password: ${DB_PASS}
hikari:
pool-name: order-hikari
maximum-pool-size: 30
minimum-idle: 8
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 3000
validation-timeout: 1000
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: false
show_sql: false
jdbc.batch_size: 100
order_inserts: true
order_updates: true
jdbc.batch_versioned_data: true
generate_statistics: falseWhy open-in-view Must Be Disabled
Transaction boundaries become coupled with interface boundaries.
Lazy loading may silently occur in the controller layer.
Connection‑pool exhaustion and hard‑to‑debug issues.
Production‑Level Order Aggregate Example
@Entity
@Table(name = "orders", indexes = {
@Index(name = "idx_order_no", columnList = "order_no", unique = true),
@Index(name = "idx_user_status_created", columnList = "user_id,status,created_at")
})
@EntityListeners(AuditingEntityListener.class)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_no", nullable = false, updatable = false, length = 64)
private String orderNo;
@Column(name = "user_id", nullable = false)
private Long userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private OrderStatus status;
@Column(name = "total_amount", precision = 18, scale = 2, nullable = false)
private BigDecimal totalAmount;
@Version
private Long version;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<OrderItem> items = new ArrayList<>();
protected Order() {}
public static Order create(String orderNo, Long userId, List<OrderItem> items) {
Order order = new Order();
order.orderNo = orderNo;
order.userId = userId;
order.status = OrderStatus.CREATED;
items.forEach(order::addItem);
order.totalAmount = order.items.stream()
.map(OrderItem::getLineAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return order;
}
public void markPaid() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Only CREATED order can be paid");
}
this.status = OrderStatus.PAID;
}
public void cancel() {
if (status == OrderStatus.SHIPPED || status == OrderStatus.COMPLETED) {
throw new IllegalStateException("Shipped or completed order cannot be cancelled");
}
this.status = OrderStatus.CANCELLED;
}
private void addItem(OrderItem item) {
item.bind(this);
this.items.add(item);
}
}Repository Is Not a "Universal DAO"
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
Optional<Order> findByOrderNo(String orderNo);
@EntityGraph(attributePaths = "items")
@Query("select o from Order o where o.id = :id")
Optional<Order> findDetailById(@Param("id") Long id);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select o from Order o where o.orderNo = :orderNo")
Optional<Order> lockByOrderNo(@Param("orderNo") String orderNo);
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update Order o set o.status = :status where o.id in :ids and o.status = :expected")
int batchTransitStatus(@Param("ids") List<Long> ids,
@Param("expected") OrderStatus expected,
@Param("status") OrderStatus status);
}High‑Concurrency Order Creation Service
@Service
@RequiredArgsConstructor
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final InventoryGateway inventoryGateway;
private final ApplicationEventPublisher publisher;
@Transactional
public String createOrder(CreateOrderCommand command) {
inventoryGateway.preCheck(command.items());
Order order = Order.create(
command.orderNo(),
command.userId(),
command.items().stream()
.map(i -> new OrderItem(i.skuId(), i.quantity(), i.unitPrice()))
.toList()
);
orderRepository.save(order);
publisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getOrderNo()));
return order.getOrderNo();
}
}Four High‑Frequency JPA Pitfalls and Solutions
1. N+1 Queries – Use @EntityGraph, join fetch, DTO projection, and never lazy‑load in the controller layer.
2. Slow Batch Writes – Enable rewriteBatchedStatements, set hibernate.jdbc.batch_size, and flush/clear in batches during massive imports.
@Transactional
public void batchCreate(List<Order> orders, EntityManager em) {
int batchSize = 100;
for (int i = 0; i < orders.size(); i++) {
em.persist(orders.get(i));
if ((i + 1) % batchSize == 0) {
em.flush();
em.clear();
}
}
}3. Long Transactions – Do not wrap remote calls, MQ or heavy computation inside a DB transaction; longer transactions increase lock‑conflict probability.
4. Using JPA as a SQL Generator – For complex reports, cross‑store aggregation or dynamic SQL, prefer MyBatis or JDBC Template and let JPA handle only the core domain.
Core Technology 2 – Redis Caching Architecture and High‑Concurrency Governance
Redis Is Deterministically Fast Under a Single‑Thread Model
Redis executes commands in a single thread, avoiding lock contention and context switches. However:
Large keys, values or transactions directly slow down the event loop.
Complex Lua scripts or long‑running blocking operations affect the whole instance.
Hot keys become a single‑shard bottleneck.
Optimization therefore focuses on controlling per‑command cost and dispersing hotspot access.
Recommended Multi‑Level Cache Architecture
Request → Local Caffeine Cache → Distributed Redis Cache → MySQL DatabaseTypical scenarios include product detail pages, store configuration, category tree, black/white list and user profile snapshots.
Production Configuration Example
spring:
data:
redis:
timeout: 2000ms
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8
max-wait: 2000ms
cluster:
nodes:
- redis-0:6379
- redis-1:6379
- redis-2:6379Production‑Grade Cache Component
@Component
@RequiredArgsConstructor
public class ProductCacheService {
private static final String PRODUCT_KEY = "product:detail:";
private static final String PRODUCT_LOCK_KEY = "lock:product:";
private static final Duration CACHE_TTL = Duration.ofMinutes(10);
private static final Duration NULL_TTL = Duration.ofMinutes(1);
private final ProductRepository productRepository;
private final StringRedisTemplate redisTemplate;
private final Cache<Long, ProductDTO> localCache = Caffeine.newBuilder()
.maximumSize(20_000)
.expireAfterWrite(Duration.ofSeconds(30))
.build();
public ProductDTO getProduct(Long productId) {
ProductDTO local = localCache.getIfPresent(productId);
if (local != null) {
return local;
}
String cacheKey = PRODUCT_KEY + productId;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if ("NULL".equals(cached)) {
return null;
}
ProductDTO dto = Jsons.fromJson(cached, ProductDTO.class);
localCache.put(productId, dto);
return dto;
}
return rebuildCache(productId, cacheKey);
}
private ProductDTO rebuildCache(Long productId, String cacheKey) {
String lockKey = PRODUCT_LOCK_KEY + productId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(3));
if (!Boolean.TRUE.equals(locked)) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(50));
return getProduct(productId);
}
try {
String again = redisTemplate.opsForValue().get(cacheKey);
if (again != null) {
return "NULL".equals(again) ? null : Jsons.fromJson(again, ProductDTO.class);
}
ProductDTO db = productRepository.findProductViewById(productId);
if (db == null) {
redisTemplate.opsForValue().set(cacheKey, "NULL", NULL_TTL);
return null;
}
redisTemplate.opsForValue().set(cacheKey, Jsons.toJson(db), randomTtl(CACHE_TTL));
localCache.put(productId, db);
return db;
} finally {
redisTemplate.delete(lockKey);
}
}
private Duration randomTtl(Duration base) {
long extraSeconds = ThreadLocalRandom.current().nextLong(30, 180);
return base.plusSeconds(extraSeconds);
}
}Why This Code Is Production‑Ready
Local Caffeine cache reduces Redis pressure.
Null‑value caching prevents cache‑penetration.
Distributed lock avoids cache‑breakdown.
Random TTL mitigates cache‑avalanche.
Double‑check after lock acquisition prevents duplicate DB hits.
Hot‑Inventory Deduction – Use Lua for Atomicity
Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));
if (stock > 0) {
redisTemplate.opsForValue().set(key, String.valueOf(stock - 1));
}The above non‑atomic pattern can cause overselling under high concurrency. Correct implementation:
public boolean deductStock(String skuKey, int amount) {
String script = """
local stock = tonumber(redis.call('GET', KEYS[1]))
local need = tonumber(ARGV[1])
if not stock or stock < need then
return 0
end
redis.call('DECRBY', KEYS[1], need)
return 1
""";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
List.of(skuKey), String.valueOf(amount));
return Long.valueOf(1L).equals(result);
}Redis Production Issue Checklist
Problem | Root Cause | Mitigation
---------------------------------------------------------------
Penetration | Requests for non‑existent data | Null‑value cache, Bloom filter
Breakdown | Hot key expires, many threads | Mutex lock, logical expiration, background warm‑up
Snowball | Massive keys expire together | Random TTL, multi‑level cache, rate limiting
Hot Key | Single key receives huge QPS | Local cache, key sharding, slot isolation
Large Key | Command payload too big | Split structure, bucket, limit value sizeCore Technology 3 – MongoDB Document Modeling and Aggregation Practice
What Data Belongs in MongoDB
Aggregate boundaries naturally map to documents.
Frequent schema changes, hard to maintain stable relational model.
Deeply nested structures where joins are expensive.
Read‑write flexibility is more important than strict relational constraints.
Typical examples: product extension attributes, comment/reply trees, user behavior trails, audit snapshots.
Product Document Model
@Document(collection = "product_snapshot")
@CompoundIndexes({
@CompoundIndex(name = "idx_category_status_created", def = "{'categoryId': 1, 'status': 1, 'createdAt': -1}"),
@CompoundIndex(name = "idx_brand_category", def = "{'brandId': 1, 'categoryId': 1}")
})
public class ProductSnapshotDocument {
@Id
private String id;
@Indexed(unique = true)
private Long productId;
private Long categoryId;
private Long brandId;
private String title;
private String subTitle;
private Map<String, Object> attributes;
private List<SkuSnapshot> skus;
private ProductStatus status;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@Data
public static class SkuSnapshot {
private Long skuId;
private String skuCode;
private BigDecimal price;
private Integer stock;
private Map<String, Object> saleAttrs;
}
}Modeling Principles
Embed When Possible – If an object is always read as a whole (e.g., product detail with SKU snapshot), keep it in a single document.
Document Size Is Not Unlimited – MongoDB limits documents to 16 MB; large arrays, endless comment streams or long‑term logs should be sharded or stored elsewhere.
Indexes Must Be Driven by Query Paths – Even in a flexible schema, query performance depends on proper indexing.
Batch Write and Upsert
@Service
@RequiredArgsConstructor
public class ProductSnapshotService {
private final MongoTemplate mongoTemplate;
public void batchUpsert(List<ProductSnapshotDocument> docs) {
BulkOperations ops = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, ProductSnapshotDocument.class);
for (ProductSnapshotDocument doc : docs) {
Query query = Query.query(Criteria.where("productId").is(doc.getProductId()));
Update update = new Update()
.set("categoryId", doc.getCategoryId())
.set("brandId", doc.getBrandId())
.set("title", doc.getTitle())
.set("subTitle", doc.getSubTitle())
.set("attributes", doc.getAttributes())
.set("skus", doc.getSkus())
.set("status", doc.getStatus())
.set("updatedAt", Instant.now())
.setOnInsert("createdAt", Instant.now());
ops.upsert(query, update);
}
ops.execute();
}
}Using UNORDERED mode is suitable for high‑throughput imports because a single failure does not block the whole batch.
Real‑World Scenario – Product Detail Snapshot Center
Generate a product‑snapshot event when an order is created.
Asynchronously build a MongoDB snapshot document.
Order‑detail service reads the snapshot first.
This demonstrates MongoDB’s classic value for read models: storing data designed for presentation, insulated from later changes to the live product record.
Core Technology 4 – Elasticsearch Search Modeling and Retrieval Pipeline
ES Provides Searchability, Sortability, Aggregation
MySQL can execute LIKE '%keyword%', but that is not a search engine. Elasticsearch advantages:
Inverted index suitable for text retrieval.
Relevance scoring.
Aggregations and facet filtering.
Advanced capabilities such as pinyin, synonyms, suggestions, geo‑location.
Search Index Model Design
@Document(indexName = "product_search_v1", createIndex = false)
public class ProductSearchDocument {
@Id
private Long productId;
@MultiField(
mainField = @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart"),
otherFields = {
@InnerField(suffix = "keyword", type = FieldType.Keyword),
@InnerField(suffix = "pinyin", type = FieldType.Text, analyzer = "pinyin_analyzer")
})
)
private String title;
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String sellingPoint;
@Field(type = FieldType.Keyword)
private String brandName;
@Field(type = FieldType.Long)
private Long categoryId;
@Field(type = FieldType.Double)
private BigDecimal minPrice;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Long)
private Long salesCount;
@Field(type = FieldType.Keyword)
private List<String> tags;
}Search Service Code
@Service
@RequiredArgsConstructor
public class ProductSearchService {
private final ElasticsearchOperations operations;
public SearchPage<ProductSearchDocument> search(ProductSearchRequest request) {
NativeQuery query = NativeQuery.builder()
.withQuery(q -> q.bool(b -> {
if (StringUtils.hasText(request.keyword())) {
b.must(m -> m.multiMatch(mm -> mm
.query(request.keyword())
.fields("title^5", "sellingPoint^2", "brandName", "tags")
.minimumShouldMatch("70%")));
}
if (request.categoryId() != null) {
b.filter(f -> f.term(t -> t.field("categoryId").value(request.categoryId())));
}
if (request.minPrice() != null || request.maxPrice() != null) {
b.filter(f -> f.range(r -> r.number(n -> {
n.field("minPrice");
if (request.minPrice() != null) n.gte(request.minPrice());
if (request.maxPrice() != null) n.lte(request.maxPrice());
return n;
})));
}
b.filter(f -> f.range(r -> r.number(n -> n.field("stock").gt(0D))));
return b;
}))
.withSort(s -> s.score(sc -> sc.order(SortOrder.Desc)))
.withPageable(PageRequest.of(request.page(), request.size()))
.build();
SearchHits<ProductSearchDocument> hits = operations.search(query, ProductSearchDocument.class);
return SearchPage.searchPageFor(hits, query.getPageable());
}
}Why Index Sync Should Not Be Synchronous Double‑Write
Writing to both the relational DB and Elasticsearch in the same transaction can cause:
DB succeeds but ES fails → data inconsistency.
ES succeeds but transaction rolls back → dirty index.
Retry logic easily creates duplicate or out‑of‑order writes.
Correct practice:
Within the DB transaction write business tables and an outbox table.
CDC or scheduled jobs push outbox events to Kafka.
Index consumer updates ES based on the event.
Consumer guarantees idempotence and version control.
Index Consumer Example
@Component
@RequiredArgsConstructor
public class ProductIndexConsumer {
private final ElasticsearchOperations operations;
private final ProductReadModelAssembler assembler;
@KafkaListener(topics = "product.changed", groupId = "product-search-indexer")
public void onMessage(ProductChangedEvent event) {
ProductSearchDocument document = assembler.assemble(event.productId());
if (document == null || event.deleted()) {
operations.delete(String.valueOf(event.productId()), ProductSearchDocument.class);
return;
}
operations.save(document);
}
}Three Production Key Points for ES
Index Versioning – Never modify mapping on a live index. Use versioned indices (e.g., product_search_v1, product_search_v2) and switch via alias.
Deep Pagination Governance – Avoid from + size for deep pages; use search_after instead.
Bulk Import Tuning – During import increase refresh_interval and optionally reduce replica count, but only within the import window.
Multi‑DataSource, Read‑Write Splitting and Access Layer Division
Why Multi‑DataSource Is Needed
High‑concurrency systems often face:
Primary DB write pressure is high while read traffic is massive.
Historical queries compete with transactional writes.
Different business domains have different data lifecycles.
Solution: adopt a multi‑data‑source strategy with primary‑replica read‑write separation, sharding by business domain, and separate read models from write models.
Recommended Division Model
Scenario | Recommended Tech
--------------------------------|-------------------------------
Aggregate writes, state flow | JPA
Complex list, reporting | MyBatis / JDBC
Hot reads | Redis
Search & filtering | Elasticsearch
Document snapshots | MongoDBA single service can host multiple data‑access technologies as long as boundaries are clear.
Read‑Write Split Configuration Example (ShardingSphere)
spring:
shardingsphere:
datasource:
names: write-ds,read-ds-0,read-ds-1
write-ds:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://mysql-master:3306/order_db
username: ${DB_USER}
password: ${DB_PASS}
read-ds-0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://mysql-slave-0:3306/order_db
username: ${DB_USER}
password: ${DB_PASS}
read-ds-1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://mysql-slave-1:3306/order_db
username: ${DB_USER}
password: ${DB_PASS}
rules:
readwrite-splitting:
data-sources:
order-rw:
static-strategy:
write-data-source-name: write-ds
read-data-source-names: read-ds-0,read-ds-1
load-balancer-name: round-robin
load-balancers:
round-robin:
type: ROUND_ROBINRead‑Write Split Engineering Traps
Master Lag – Immediately reading after order creation may miss the just‑written row on a replica. Solutions: read core link from master, use short‑term sticky reads, or accept second‑level delay for non‑critical data.
Forcing Reads from Replica Inside a Transaction – Dangerous; reads and writes in a transaction must follow the same routing strategy.
Sending All Queries to Replicas – Heavy reports or export queries should still go to an isolated analytics DB.
Distributed Transactions – Use 2PC Sparingly, Prefer Business Decomposition & Eventual Consistency
Do You Really Need Distributed Transactions?
When crossing databases or services, the instinct is to reach for Seata or 2PC. A mature architect first asks:
Is this a core strong‑consistency link?
Can the business be split to avoid cross‑service transactions?
Can the problem be transformed into eventual consistency?
Order Scenario Decomposition
Inventory validation.
Order persistence.
Inventory lock.
Payment order creation.
Push marketing, points, notifications.
Strong consistency is usually required only for "order status + core inventory lock". Everything else can be asynchronous.
Recommended Model – Local Transaction + Outbox
@Entity
@Table(name = "event_outbox", indexes = {@Index(name = "idx_event_status_created", columnList = "status,created_at")})
public class EventOutbox {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 64)
private String eventType;
@Column(nullable = false, length = 64)
private String aggregateType;
@Column(nullable = false, length = 64)
private String aggregateId;
@Lob
@Column(nullable = false)
private String payload;
@Enumerated(EnumType.STRING)
private OutboxStatus status;
@CreatedDate
private LocalDateTime createdAt;
}
@Transactional
public String createOrder(CreateOrderCommand command) {
Order order = Order.create(command.orderNo(), command.userId(), mapItems(command));
orderRepository.save(order);
EventOutbox outbox = new EventOutbox(
"OrderCreated",
"Order",
order.getId().toString(),
Jsons.toJson(new OrderCreatedEvent(order.getId(), order.getOrderNo()))
);
outboxRepository.save(outbox);
return order.getOrderNo();
}An external dispatcher or CDC reads the outbox table and pushes events to Kafka, guaranteeing that DB state and pending events are committed atomically.
If You Must Use Seata, Beware
Introduces global locks.
Creates undo_log tables.
Long transactions amplify lock wait times.
Performance spikes under flash‑sale loads.
Seata is suitable only for low‑volume, short‑duration, few‑branch scenarios. It is unsuitable for flash‑sale spikes, massive batch orders, long‑running user interactions or highly unstable remote calls.
Consumer Idempotence Is Mandatory
@Service
@RequiredArgsConstructor
public class InventoryEventHandler {
private final IdempotentRepository idempotentRepository;
private final InventoryService inventoryService;
@Transactional
public void handle(OrderCreatedEvent event) {
if (idempotentRepository.existsByMessageId(event.eventId())) {
return;
}
inventoryService.freeze(event.orderNo(), event.items());
idempotentRepository.save(new IdempotentRecord(event.eventId()));
}
}Message systems inevitably bring duplicates, out‑of‑order delivery and delays; consumption must be idempotent.
CDC & Asynchronous Data Sync – Decoupling Double‑Write
Why CDC Matters
In a multi‑store architecture the hardest problem is keeping all stores eventually consistent. CDC provides:
Capture data changes from DB logs.
Business code stays free of sync concerns.
Unified driving of ES, MongoDB, cache warm‑up, data lake, etc.
Recommended Pipeline
MySQL Binlog → Debezium → Kafka → Downstream Consumers
→ Elasticsearch index update
→ MongoDB snapshot update
→ Redis cache invalidationIndex Sync Consumer Example
@Component
@RequiredArgsConstructor
public class OrderIndexSyncConsumer {
private final ElasticsearchOperations operations;
@KafkaListener(topics = "dbserver1.order_db.orders", groupId = "order-search-sync")
public void onMessage(String payload) {
DebeziumEnvelope<OrderRow> envelope = Jsons.fromJson(payload,
new TypeReference<DebeziumEnvelope<OrderRow>>() {});
String op = envelope.op();
if ("d".equals(op)) {
operations.delete(String.valueOf(envelope.before().id()), OrderSearchDocument.class);
return;
}
OrderRow row = envelope.after();
OrderSearchDocument doc = OrderSearchDocument.from(row);
operations.save(doc);
}
}Three Governance Points for CDC Sync
Idempotence – repeated consumption must not cause side effects.
Order – the same aggregate should be processed in order within a partition.
Replay – consumers must support rebuilding state for fault recovery or index reconstruction.
High‑Concurrency Engineering Upgrade – System‑Level Cooperation
Four‑Layer Defense for High‑Concurrency Systems
Layer 1: Entry Throttling – Gateway rate limiting, user‑level anti‑scraping, hot‑activity reservation.
Layer 2: Cache Pressure Relief – Local Caffeine cache, Redis hotspot cache, logical expiration with async refresh.
Layer 3: Asynchronous Decoupling – Post‑order coupon issuance, async index building, async statistics sync.
Layer 4: Database Protection – Short transactions, covering indexes, batch writes, hot‑key sharding and table partitioning.
Production‑Grade Flash‑Sale Order Flow
User Request → Gateway Rate‑Limit → Redis Pre‑Deduction (Lua) → Write Order Message to Kafka →
Order Service Asynchronously Creates Order → MySQL Persist → Outbox / Event Dispatch →
Inventory, Payment, Notification Systems ConsumeKey ideas:
Put the most contested high‑concurrency part (stock deduction) into Redis atomically.
Separate DB writes from the synchronous request path.
Use messaging to smooth spikes and achieve eventual consistency.
Thread‑Pool & Connection‑Pool Coordination
Many bottlenecks stem from imbalance among thread pools, DB connection pools and downstream latency. Design principles:
Do not blindly increase web‑container threads.
Size the DB connection pool based on max connections and average SQL response time.
Isolate async thread pools per business type.
Effective throughput ≈ connections / average SQL response time. If average RT jumps from 10 ms to 100 ms, throughput drops an order of magnitude.
Production‑Level Observability – Know Where the System Is Slow
Minimum Monitoring Checklist
Database – Connection‑pool usage, slow‑SQL count, lock wait time, transactions per second.
Redis – Hit rate, instance QPS, slow queries, hot‑key distribution.
Elasticsearch – Query latency, refresh/merge time, segment count, JVM heap usage.
Kafka / CDC – Consumer lag, retry attempts, dead‑letter queue size.
Spring Boot Recommended Integration
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
probes:
enabled: true
metrics:
tags:
application: order-serviceCommon stacks: Micrometer + Prometheus + Grafana; SkyWalking / OpenTelemetry; ELK / Loki for log search.
Full‑Trace Requirement
A complete request trace should include:
Controller latency.
Service latency.
SQL count and total SQL time.
Redis operation latency.
Kafka send/consume latency.
Elasticsearch query latency.
Without end‑to‑end tracing, high‑concurrency tuning is blind.
Kubernetes & Cloud‑Native Deployment Essentials
Container Image Baseline
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /workspace
COPY . .
RUN ./mvnw -B clean package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd -r -u 1001 spring
USER spring
COPY --from=builder /workspace/target/*.jar app.jar
ENTRYPOINT ["java","-XX:+UseG1GC","-XX:MaxRAMPercentage=75","-jar","/app/app.jar"]Deployment Example
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 6
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
env:
- name: JAVA_TOOL_OPTIONS
value: "-Duser.timezone=Asia/Shanghai"Three Deployment Principles
Stateless application; state lives in data layers.
Separate liveness and readiness probes.
Graceful shutdown to avoid traffic cut‑off during rolling updates.
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30sEvolution Roadmap – From Monolith to Distributed Data Access
Stage 1: Monolith
MySQL + Redis.
JPA for core transactions.
MyBatis for complex queries.
Suitable for small‑to‑medium scale; focus on establishing correct boundaries.
Stage 2: Read‑Write Split & Search Off‑load
Primary‑replica DB.
Redis cluster.
Standalone Elasticsearch for search.
Solves read‑traffic and search‑traffic pressure.
Stage 3: Service Decomposition & Asynchronous Decoupling
Separate order, inventory, product, payment services.
Kafka‑driven async collaboration.
Outbox / CDC for unified sync.
Stage 4: Multi‑Modal Data & CQRS
MongoDB for document read models.
Elasticsearch for search read models.
CQRS clearly separates read and write responsibilities.
Data‑access layer becomes a complete data architecture.
The evolution is not about stacking technologies but about moving each access pattern to the storage that best fits its responsibility.
Conclusion – Five Principles for Production‑Grade Data Access
Use the right store for the right responsibility – never force a single database to solve every problem.
Strong consistency for core transaction paths; eventual consistency for peripheral paths.
Avoid synchronous double‑write; drive multi‑store sync via events & CDC.
High‑concurrency optimisation is system‑level cooperation, not single‑point tuning.
Observability is part of the data‑access system; without metrics, logs and tracing true production optimisation cannot happen.
Team Roll‑out Recommendations
Implement the following steps in order:
Map business boundaries into strong‑consistency and eventual‑consistency zones.
Establish JPA aggregate roots and short‑transaction conventions for core flows.
Build multi‑level cache (local + Redis) and define cache‑governance policies for hotspot reads.
Construct ES / MongoDB sync pipelines for search and presentation read models.
Replace business double‑writes with outbox or CDC mechanisms.
Complete chain‑trace, metrics and dead‑letter handling.
This article assumes Spring Boot 3.x, Java 21, Spring Data JPA, Spring Data Redis, Spring Data MongoDB and Spring Data Elasticsearch. In practice, mixing JPA with MyBatis, coordinating Redis with local cache, and pairing CDC with a message system are the most common and stable implementation patterns.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
