๐ฏ Introduction
Data consistency is one of the most critical challenges in modern Java enterprise applications. As systems scale and become distributed, maintaining data integrity while ensuring performance becomes increasingly complex. This comprehensive guide explores practical data consistency patterns implemented in real-world Java applications, complete with case studies, implementation details, and detailed trade-off analysis.
๐ The Data Consistency Challenge
๐ Understanding Data Consistency Levels
Data consistency refers to the guarantee that all nodes in a distributed system see the same data at the same time. In Java enterprise applications, we typically encounter several consistency models:
graph TD
A[Data Consistency Models] --> B[Strong Consistency]
A --> C[Eventual Consistency]
A --> D[Weak Consistency]
A --> E[Session Consistency]
B --> F[ACID Transactions]
B --> G[Two-Phase Commit]
C --> H[BASE Properties]
C --> I[Asynchronous Replication]
D --> J[Best Effort Delivery]
E --> K[Read Your Writes]
E --> L[Monotonic Reads]
style A fill:#ff6b6b
style B fill:#4ecdc4
style C fill:#45b7d1
๐จ Common Consistency Problems
Race Conditions in Concurrent Operations:
- Multiple users booking the same hotel room
- Inventory depletion during high-traffic sales
- Financial transaction conflicts
- Cache invalidation inconsistencies
Distributed System Challenges:
- Network partitions between microservices
- Service failures during transaction processing
- Clock synchronization issues
- Message delivery guarantees
๐๏ธ Core Data Consistency Patterns
๐ 1. Optimistic Locking Pattern
Optimistic locking assumes that conflicts are rare and checks for conflicts only when committing changes. This pattern is ideal for scenarios with low contention and high read-to-write ratios.
๐ Implementation Architecture
graph TD
A[Client Request] --> B[Read Data + Version]
B --> C[Business Logic Processing]
C --> D[Update with Version Check]
D --> E{Version Match?}
E -->|Yes| F[Commit Transaction]
E -->|No| G[OptimisticLockException]
G --> H[Retry Logic]
H --> B
F --> I[Success Response]
style E fill:#feca57
style G fill:#ff6b35
style F fill:#4ecdc4
๐ ๏ธ Java Implementation
Entity with Version Control:
1@Entity
2@Table(name = "hotel_rooms")
3public class Room {
4 @Id
5 @GeneratedValue(strategy = GenerationType.IDENTITY)
6 private Long id;
7
8 @Version
9 private Long version;
10
11 @Column(nullable = false)
12 private String roomNumber;
13
14 @Column(nullable = false)
15 private Boolean isAvailable;
16
17 @Column(nullable = false)
18 private BigDecimal pricePerNight;
19
20 @OneToMany(mappedBy = "room", cascade = CascadeType.ALL)
21 private List<Booking> bookings = new ArrayList<>();
22
23 // Constructors, getters, setters
24 public void markAsBooked() {
25 this.isAvailable = false;
26 }
27
28 public void markAsAvailable() {
29 this.isAvailable = true;
30 }
31}
32
33@Entity
34@Table(name = "bookings")
35public class Booking {
36 @Id
37 @GeneratedValue(strategy = GenerationType.IDENTITY)
38 private Long id;
39
40 @Version
41 private Long version;
42
43 @ManyToOne(fetch = FetchType.LAZY)
44 @JoinColumn(name = "room_id", nullable = false)
45 private Room room;
46
47 @Column(nullable = false)
48 private String guestName;
49
50 @Column(nullable = false)
51 private LocalDateTime checkInDate;
52
53 @Column(nullable = false)
54 private LocalDateTime checkOutDate;
55
56 @Enumerated(EnumType.STRING)
57 private BookingStatus status;
58
59 // Constructors, getters, setters
60}
Service Layer with Optimistic Locking:
1@Service
2@Transactional
3public class HotelBookingService {
4
5 private final RoomRepository roomRepository;
6 private final BookingRepository bookingRepository;
7 private final BookingMetrics bookingMetrics;
8
9 public BookingResponse bookRoom(BookingRequest request) {
10 try {
11 // Read room with current version
12 Room room = roomRepository.findById(request.getRoomId())
13 .orElseThrow(() -> new RoomNotFoundException(request.getRoomId()));
14
15 // Business logic validation
16 validateBookingRequest(room, request);
17
18 // Create booking and update room
19 Booking booking = createBooking(room, request);
20 room.markAsBooked();
21
22 // This will trigger version check
23 roomRepository.save(room);
24 bookingRepository.save(booking);
25
26 bookingMetrics.incrementSuccessfulBookings();
27
28 return BookingResponse.success(booking);
29
30 } catch (OptimisticLockException e) {
31 bookingMetrics.incrementOptimisticLockFailures();
32 throw new ConcurrentBookingException(
33 "Room was modified by another user. Please try again."
34 );
35 }
36 }
37
38 @Retryable(
39 value = {ConcurrentBookingException.class},
40 maxAttempts = 3,
41 backoff = @Backoff(delay = 100, multiplier = 2)
42 )
43 public BookingResponse bookRoomWithRetry(BookingRequest request) {
44 return bookRoom(request);
45 }
46
47 private void validateBookingRequest(Room room, BookingRequest request) {
48 if (!room.getIsAvailable()) {
49 throw new RoomNotAvailableException(room.getId());
50 }
51
52 if (hasDateConflict(room, request.getCheckInDate(), request.getCheckOutDate())) {
53 throw new DateConflictException("Room is already booked for the requested dates");
54 }
55 }
56}
Global Exception Handler:
1@RestControllerAdvice
2public class GlobalExceptionHandler {
3
4 @ExceptionHandler(ConcurrentBookingException.class)
5 public ResponseEntity<ErrorResponse> handleConcurrentBooking(
6 ConcurrentBookingException e) {
7 return ResponseEntity.status(HttpStatus.CONFLICT)
8 .body(new ErrorResponse(
9 "CONCURRENT_MODIFICATION",
10 e.getMessage(),
11 "Please refresh and try again"
12 ));
13 }
14
15 @ExceptionHandler(OptimisticLockException.class)
16 public ResponseEntity<ErrorResponse> handleOptimisticLock(
17 OptimisticLockException e) {
18 return ResponseEntity.status(HttpStatus.CONFLICT)
19 .body(new ErrorResponse(
20 "OPTIMISTIC_LOCK_FAILURE",
21 "Data was modified by another process",
22 "Please refresh and retry your operation"
23 ));
24 }
25
26 public static class ErrorResponse {
27 private String errorCode;
28 private String message;
29 private String suggestion;
30
31 // Constructors, getters, setters
32 }
33}
โ Optimistic Locking Pros & Cons
Advantages:
- High Performance: No locks held during business logic processing
- Scalability: Supports high concurrent read operations
- Deadlock Prevention: No lock acquisition ordering issues
- Resource Efficiency: Minimal database connection usage
Disadvantages:
- Retry Complexity: Requires sophisticated retry mechanisms
- High Contention Issues: Performance degrades with frequent conflicts
- User Experience: May require multiple submission attempts
- Lost Updates: Risk of losing work in high-contention scenarios
Best Use Cases:
- Read-heavy applications with occasional updates
- Content management systems
- User profile management
- Configuration data updates
๐ 2. Pessimistic Locking Pattern
Pessimistic locking assumes conflicts are likely and prevents them by acquiring locks before performing operations.
๐ Pessimistic Locking Architecture
graph TD
A[Client Request] --> B[Acquire Lock]
B --> C{Lock Available?}
C -->|No| D[Wait/Timeout]
D --> E[Lock Timeout Exception]
C -->|Yes| F[Execute Business Logic]
F --> G[Perform Database Updates]
G --> H[Release Lock]
H --> I[Success Response]
style C fill:#feca57
style E fill:#ff6b35
style F fill:#4ecdc4
style H fill:#96ceb4
๐ ๏ธ Java Implementation
Repository with Pessimistic Locking:
1@Repository
2public interface RoomRepository extends JpaRepository<Room, Long> {
3
4 @Lock(LockModeType.PESSIMISTIC_WRITE)
5 @Query("SELECT r FROM Room r WHERE r.id = :id")
6 Optional<Room> findByIdWithPessimisticLock(@Param("id") Long id);
7
8 @Lock(LockModeType.PESSIMISTIC_READ)
9 @Query("SELECT r FROM Room r WHERE r.isAvailable = true")
10 List<Room> findAvailableRoomsWithReadLock();
11
12 @Modifying
13 @Query("UPDATE Room r SET r.isAvailable = :available WHERE r.id = :id")
14 int updateRoomAvailability(@Param("id") Long id, @Param("available") boolean available);
15}
Service with Pessimistic Locking Strategy:
1@Service
2@Transactional
3public class PessimisticBookingService {
4
5 private final RoomRepository roomRepository;
6 private final BookingRepository bookingRepository;
7 private final BookingMetrics bookingMetrics;
8
9 @Transactional(timeout = 30) // 30-second timeout
10 public BookingResponse bookRoomWithPessimisticLock(BookingRequest request) {
11 try {
12 // Acquire pessimistic write lock
13 Room room = roomRepository.findByIdWithPessimisticLock(request.getRoomId())
14 .orElseThrow(() -> new RoomNotFoundException(request.getRoomId()));
15
16 // Validation under lock
17 validateRoomAvailability(room, request);
18
19 // Create booking and update room atomically
20 Booking booking = createBookingAndUpdateRoom(room, request);
21
22 bookingMetrics.incrementSuccessfulBookings();
23 return BookingResponse.success(booking);
24
25 } catch (PessimisticLockingFailureException e) {
26 bookingMetrics.incrementLockTimeoutFailures();
27 throw new BookingLockException("Unable to acquire room lock. Please try again.");
28 }
29 }
30
31 private Booking createBookingAndUpdateRoom(Room room, BookingRequest request) {
32 // All operations under the same pessimistic lock
33 Booking booking = Booking.builder()
34 .room(room)
35 .guestName(request.getGuestName())
36 .checkInDate(request.getCheckInDate())
37 .checkOutDate(request.getCheckOutDate())
38 .status(BookingStatus.CONFIRMED)
39 .build();
40
41 room.markAsBooked();
42 room.getBookings().add(booking);
43
44 bookingRepository.save(booking);
45 roomRepository.save(room);
46
47 return booking;
48 }
49}
Advanced Lock Management:
1@Component
2public class LockManager {
3
4 private final ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
5 private final MeterRegistry meterRegistry;
6
7 public <T> T executeWithLock(String lockKey, Supplier<T> operation, Duration timeout) {
8 ReentrantLock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
9
10 Timer.Sample sample = Timer.start(meterRegistry);
11 try {
12 if (!lock.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
13 throw new LockAcquisitionException("Failed to acquire lock: " + lockKey);
14 }
15
16 return operation.get();
17
18 } catch (InterruptedException e) {
19 Thread.currentThread().interrupt();
20 throw new LockInterruptedException("Lock operation interrupted", e);
21 } finally {
22 if (lock.isHeldByCurrentThread()) {
23 lock.unlock();
24 }
25 sample.stop(Timer.builder("lock.execution.time")
26 .tag("lock.key", lockKey)
27 .register(meterRegistry));
28 }
29 }
30
31 @Scheduled(fixedRate = 300000) // Clean up every 5 minutes
32 public void cleanupUnusedLocks() {
33 lockMap.entrySet().removeIf(entry ->
34 !entry.getValue().isLocked() && !entry.getValue().hasQueuedThreads()
35 );
36 }
37}
โ Pessimistic Locking Pros & Cons
Advantages:
- Guaranteed Consistency: Prevents all concurrent modification issues
- Predictable Behavior: No unexpected failures due to conflicts
- Data Integrity: Strong consistency guarantees
- Simple Error Handling: Clear lock acquisition success/failure
Disadvantages:
- Performance Impact: Reduced concurrency and throughput
- Deadlock Risk: Potential for deadlocks with multiple locks
- Lock Contention: Threads blocked waiting for locks
- Timeout Management: Complex timeout and retry logic needed
Best Use Cases:
- Financial transactions and payment processing
- Inventory management with limited quantities
- Critical resource allocation
- Sequential processing requirements
๐ 3. Hybrid Locking Strategy
A sophisticated approach that dynamically selects locking strategies based on system conditions and contention levels.
๐ Hybrid Strategy Architecture
graph TD
A[Incoming Request] --> B[Analyze System Load]
B --> C{Contention Level}
C -->|Low| D[Optimistic Locking]
C -->|Medium| E[Adaptive Strategy]
C -->|High| F[Pessimistic Locking]
D --> G[Version-Based Update]
E --> H[Dynamic Selection]
F --> I[Lock-Based Update]
G --> J[Success/Retry]
H --> K[Monitor & Adjust]
I --> L[Guaranteed Success]
style C fill:#feca57
style E fill:#96ceb4
style K fill:#ff9ff3
๐ ๏ธ Java Implementation
Contention Metrics and Strategy Selection:
1@Component
2public class BookingMetrics {
3
4 private final MeterRegistry meterRegistry;
5 private final ConcurrentHashMap<Long, AtomicLong> roomContentionMap = new ConcurrentHashMap<>();
6
7 public void recordBookingAttempt(Long roomId) {
8 roomContentionMap.computeIfAbsent(roomId, k -> new AtomicLong(0)).incrementAndGet();
9 meterRegistry.counter("booking.attempts", "room", roomId.toString()).increment();
10 }
11
12 public void recordOptimisticLockFailure(Long roomId) {
13 meterRegistry.counter("booking.optimistic.failures", "room", roomId.toString()).increment();
14 }
15
16 public LockingStrategy recommendStrategy(Long roomId) {
17 long contentionLevel = roomContentionMap.getOrDefault(roomId, new AtomicLong(0)).get();
18
19 if (contentionLevel > 10) {
20 return LockingStrategy.PESSIMISTIC;
21 } else if (contentionLevel > 5) {
22 return LockingStrategy.HYBRID;
23 } else {
24 return LockingStrategy.OPTIMISTIC;
25 }
26 }
27
28 @Scheduled(fixedRate = 60000) // Reset every minute
29 public void resetContentionMetrics() {
30 roomContentionMap.clear();
31 }
32}
Adaptive Booking Service:
1@Service
2@Transactional
3public class AdaptiveBookingService {
4
5 private final OptimisticBookingService optimisticService;
6 private final PessimisticBookingService pessimisticService;
7 private final BookingMetrics bookingMetrics;
8
9 public BookingResponse bookRoom(BookingRequest request) {
10 LockingStrategy strategy = bookingMetrics.recommendStrategy(request.getRoomId());
11
12 return switch (strategy) {
13 case OPTIMISTIC -> bookWithOptimisticStrategy(request);
14 case PESSIMISTIC -> bookWithPessimisticStrategy(request);
15 case HYBRID -> bookWithHybridStrategy(request);
16 };
17 }
18
19 private BookingResponse bookWithHybridStrategy(BookingRequest request) {
20 int maxRetries = 3;
21 int retryCount = 0;
22
23 while (retryCount < maxRetries) {
24 try {
25 // Start with optimistic approach
26 return optimisticService.bookRoom(request);
27
28 } catch (ConcurrentBookingException e) {
29 retryCount++;
30 bookingMetrics.recordOptimisticLockFailure(request.getRoomId());
31
32 // Switch to pessimistic after 2 failures
33 if (retryCount >= 2) {
34 return pessimisticService.bookRoomWithPessimisticLock(request);
35 }
36
37 // Exponential backoff
38 try {
39 Thread.sleep(100L * (1L << retryCount));
40 } catch (InterruptedException ie) {
41 Thread.currentThread().interrupt();
42 throw new BookingInterruptedException("Booking process interrupted", ie);
43 }
44 }
45 }
46
47 throw new BookingFailedException("Failed to book room after maximum retries");
48 }
49}
๐ Distributed Transaction Patterns
๐ Two-Phase Commit (2PC) Implementation
For distributed systems requiring strong consistency across multiple services, the Two-Phase Commit protocol provides ACID guarantees at the cost of increased complexity and reduced availability.
๐ 2PC Architecture
graph TD
A[Transaction Coordinator] --> B[Phase 1: Prepare]
B --> C[Room Service]
B --> D[Payment Service]
B --> E[Notification Service]
C --> F[Prepare Response]
D --> G[Prepare Response]
E --> H[Prepare Response]
F --> I[Phase 2: Decision]
G --> I
H --> I
I --> J{All Prepared?}
J -->|Yes| K[Commit Command]
J -->|No| L[Rollback Command]
K --> M[Room Service Commit]
K --> N[Payment Service Commit]
K --> O[Notification Service Commit]
style I fill:#feca57
style J fill:#ff6b35
style K fill:#4ecdc4
style L fill:#ff9ff3
๐ ๏ธ Java Implementation
Transaction Coordinator:
1@Component
2public class BookingTransactionCoordinator {
3
4 private final List<TransactionParticipant> participants;
5 private final TransactionLogRepository transactionLogRepository;
6
7 @Transactional
8 public BookingResult executeDistributedBooking(DistributedBookingRequest request) {
9 String transactionId = UUID.randomUUID().toString();
10 TransactionContext context = new TransactionContext(transactionId, request);
11
12 try {
13 // Phase 1: Prepare
14 logTransactionStart(context);
15 boolean allPrepared = executePhaseOne(context);
16
17 if (!allPrepared) {
18 // Phase 2: Rollback
19 executeRollback(context);
20 return BookingResult.failed("One or more services failed to prepare");
21 }
22
23 // Phase 2: Commit
24 boolean allCommitted = executePhaseTwo(context);
25
26 if (allCommitted) {
27 logTransactionCommit(context);
28 return BookingResult.success(context.getBookingId());
29 } else {
30 // Handle partial commit scenario
31 handlePartialCommit(context);
32 return BookingResult.partialFailure("Partial commit occurred");
33 }
34
35 } catch (Exception e) {
36 executeRollback(context);
37 logTransactionFailure(context, e);
38 return BookingResult.failed("Transaction failed: " + e.getMessage());
39 }
40 }
41
42 private boolean executePhaseOne(TransactionContext context) {
43 List<CompletableFuture<PrepareResponse>> futures = participants.stream()
44 .map(participant -> CompletableFuture.supplyAsync(() -> {
45 try {
46 return participant.prepare(context);
47 } catch (Exception e) {
48 return PrepareResponse.failure(participant.getServiceId(), e.getMessage());
49 }
50 }))
51 .toList();
52
53 // Wait for all participants with timeout
54 try {
55 List<PrepareResponse> responses = futures.stream()
56 .map(future -> future.orTimeout(30, TimeUnit.SECONDS))
57 .map(CompletableFuture::join)
58 .toList();
59
60 return responses.stream().allMatch(PrepareResponse::isSuccess);
61
62 } catch (CompletionException e) {
63 return false; // Timeout or other failure
64 }
65 }
66
67 private boolean executePhaseTwo(TransactionContext context) {
68 List<CompletableFuture<CommitResponse>> futures = participants.stream()
69 .map(participant -> CompletableFuture.supplyAsync(() -> {
70 try {
71 return participant.commit(context);
72 } catch (Exception e) {
73 return CommitResponse.failure(participant.getServiceId(), e.getMessage());
74 }
75 }))
76 .toList();
77
78 List<CommitResponse> responses = futures.stream()
79 .map(CompletableFuture::join)
80 .toList();
81
82 return responses.stream().allMatch(CommitResponse::isSuccess);
83 }
84}
Transaction Participant (Room Service):
1@Service
2public class RoomServiceParticipant implements TransactionParticipant {
3
4 private final RoomRepository roomRepository;
5 private final Map<String, Room> preparedRooms = new ConcurrentHashMap<>();
6
7 @Override
8 public PrepareResponse prepare(TransactionContext context) {
9 try {
10 Long roomId = context.getRequest().getRoomId();
11 Room room = roomRepository.findByIdWithPessimisticLock(roomId)
12 .orElseThrow(() -> new RoomNotFoundException(roomId));
13
14 if (!room.getIsAvailable()) {
15 return PrepareResponse.failure(getServiceId(), "Room not available");
16 }
17
18 // Reserve room but don't commit yet
19 preparedRooms.put(context.getTransactionId(), room);
20
21 return PrepareResponse.success(getServiceId());
22
23 } catch (Exception e) {
24 return PrepareResponse.failure(getServiceId(), e.getMessage());
25 }
26 }
27
28 @Override
29 public CommitResponse commit(TransactionContext context) {
30 try {
31 Room room = preparedRooms.remove(context.getTransactionId());
32 if (room == null) {
33 return CommitResponse.failure(getServiceId(), "No prepared room found");
34 }
35
36 room.markAsBooked();
37 roomRepository.save(room);
38
39 return CommitResponse.success(getServiceId());
40
41 } catch (Exception e) {
42 return CommitResponse.failure(getServiceId(), e.getMessage());
43 }
44 }
45
46 @Override
47 public void rollback(TransactionContext context) {
48 // Clean up any prepared state
49 preparedRooms.remove(context.getTransactionId());
50 }
51
52 @Override
53 public String getServiceId() {
54 return "room-service";
55 }
56}
Transaction Data Structures:
1public class TransactionContext {
2 private final String transactionId;
3 private final DistributedBookingRequest request;
4 private final LocalDateTime startTime;
5 private TransactionState state;
6 private String bookingId;
7
8 public enum TransactionState {
9 ACTIVE, PREPARING, COMMITTING, ROLLING_BACK, COMMITTED, ROLLED_BACK
10 }
11}
12
13public class PrepareResponse {
14 private final String participantId;
15 private final boolean success;
16 private final String message;
17 private final Map<String, Object> metadata;
18
19 public static PrepareResponse success(String participantId) {
20 return new PrepareResponse(participantId, true, "Prepared successfully", Map.of());
21 }
22
23 public static PrepareResponse failure(String participantId, String message) {
24 return new PrepareResponse(participantId, false, message, Map.of());
25 }
26}
27
28public class TransactionLogEntry {
29 private String transactionId;
30 private String participantId;
31 private TransactionPhase phase;
32 private ParticipantState state;
33 private LocalDateTime timestamp;
34 private String details;
35
36 public enum TransactionPhase { PREPARE, COMMIT, ROLLBACK }
37 public enum ParticipantState { WORKING, PREPARED, COMMITTED, ROLLED_BACK, ABORTED }
38}
โ Two-Phase Commit Pros & Cons
Advantages:
- Strong Consistency: Guarantees ACID properties across distributed systems
- Data Integrity: All-or-nothing transaction semantics
- Atomic Operations: Ensures consistency across multiple services
- Recovery Support: Well-defined recovery procedures
Disadvantages:
- Availability Impact: Blocking protocol reduces system availability
- Performance Overhead: Multiple round trips increase latency
- Coordinator Bottleneck: Single point of failure
- Complexity: Complex failure handling and recovery logic
Best Use Cases:
- Financial systems requiring strict consistency
- Critical business processes
- Regulatory compliance scenarios
- Systems with infrequent distributed transactions
๐ Performance Analysis and Trade-offs
๐ Consistency vs. Performance Trade-offs
graph TD
A[Consistency Requirements] --> B{Application Type}
B -->|Financial/Critical| C[Strong Consistency]
B -->|Social/Content| D[Eventual Consistency]
B -->|Mixed Requirements| E[Hybrid Approach]
C --> F[2PC/Saga Pattern]
C --> G[Pessimistic Locking]
D --> H[Async Replication]
D --> I[CQRS/Event Sourcing]
E --> J[Optimistic + Retry]
E --> K[Dynamic Strategy]
F --> L[High Latency, Strong Guarantees]
G --> L
H --> M[Low Latency, Weak Guarantees]
I --> M
J --> N[Balanced Approach]
K --> N
style C fill:#ff6b35
style D fill:#4ecdc4
style E fill:#feca57
๐ Performance Benchmarks
Throughput Comparison (Requests/Second):
Strategy | Low Contention | Medium Contention | High Contention |
---|---|---|---|
Optimistic | 1,200 | 800 | 300 |
Pessimistic | 800 | 700 | 650 |
Hybrid | 1,100 | 900 | 600 |
2PC | 400 | 300 | 200 |
Latency Analysis (95th Percentile):
Strategy | Average Latency | P95 Latency | P99 Latency |
---|---|---|---|
Optimistic | 50ms | 200ms | 500ms |
Pessimistic | 100ms | 300ms | 600ms |
Hybrid | 60ms | 250ms | 400ms |
2PC | 300ms | 800ms | 1200ms |
๐ฏ Strategy Selection Guidelines
Choose Optimistic Locking When:
- Read-to-write ratio is high (>10:1)
- Conflicts are rare (<5% of operations)
- User can tolerate retry scenarios
- Performance is critical
Choose Pessimistic Locking When:
- High contention scenarios (>20% conflicts)
- Data consistency is critical
- Retry logic is complex to implement
- Lock duration is short
Choose Hybrid Strategy When:
- Variable load patterns
- Mixed consistency requirements
- Need adaptive behavior
- Want to optimize for both scenarios
Choose 2PC When:
- Strong consistency across services required
- ACID properties are mandatory
- Can tolerate increased latency
- Have robust failure recovery mechanisms
๐ ๏ธ Implementation Best Practices
๐ง Configuration and Monitoring
Application Configuration:
1# application.yml
2spring:
3 datasource:
4 hikari:
5 maximum-pool-size: 50
6 minimum-idle: 10
7 connection-timeout: 30000
8 idle-timeout: 600000
9 max-lifetime: 1800000
10
11 jpa:
12 properties:
13 hibernate:
14 dialect: org.hibernate.dialect.PostgreSQLDialect
15 format_sql: true
16 show_sql: false
17 jdbc:
18 lock:
19 timeout: 30000 # 30 seconds
20 query:
21 timeout: 10000 # 10 seconds
22
23booking:
24 consistency:
25 strategy: HYBRID
26 optimistic:
27 max-retries: 3
28 backoff-multiplier: 2
29 pessimistic:
30 lock-timeout: 30000
31 hybrid:
32 contention-threshold: 5
33 metrics:
34 cleanup-interval: 60000
35
36management:
37 endpoints:
38 web:
39 exposure:
40 include: health, metrics, prometheus
41 metrics:
42 export:
43 prometheus:
44 enabled: true
Monitoring and Alerting:
1@Component
2public class ConsistencyMetrics {
3
4 private final MeterRegistry meterRegistry;
5
6 public void recordTransactionAttempt(String strategy) {
7 meterRegistry.counter("transaction.attempts", "strategy", strategy).increment();
8 }
9
10 public void recordTransactionSuccess(String strategy, Duration duration) {
11 meterRegistry.timer("transaction.duration", "strategy", strategy).record(duration);
12 meterRegistry.counter("transaction.success", "strategy", strategy).increment();
13 }
14
15 public void recordTransactionFailure(String strategy, String reason) {
16 meterRegistry.counter("transaction.failures",
17 "strategy", strategy,
18 "reason", reason).increment();
19 }
20
21 public void recordLockContention(String resourceType, long waitTime) {
22 meterRegistry.timer("lock.contention.wait",
23 "resource", resourceType).record(waitTime, TimeUnit.MILLISECONDS);
24 }
25}
๐งช Testing Strategies
Concurrency Testing:
1@SpringBootTest
2class ConcurrencyIntegrationTest {
3
4 @Autowired
5 private AdaptiveBookingService bookingService;
6
7 @Autowired
8 private RoomRepository roomRepository;
9
10 @Test
11 void testConcurrentBookingWithOptimisticLocking() throws InterruptedException {
12 // Setup
13 Room room = createTestRoom();
14 int threadCount = 10;
15 CountDownLatch latch = new CountDownLatch(threadCount);
16 AtomicInteger successCount = new AtomicInteger(0);
17 AtomicInteger failureCount = new AtomicInteger(0);
18
19 // Execute concurrent bookings
20 ExecutorService executor = Executors.newFixedThreadPool(threadCount);
21 for (int i = 0; i < threadCount; i++) {
22 final int threadId = i;
23 executor.submit(() -> {
24 try {
25 BookingRequest request = BookingRequest.builder()
26 .roomId(room.getId())
27 .guestName("Guest-" + threadId)
28 .checkInDate(LocalDateTime.now().plusDays(1))
29 .checkOutDate(LocalDateTime.now().plusDays(2))
30 .build();
31
32 bookingService.bookRoom(request);
33 successCount.incrementAndGet();
34
35 } catch (Exception e) {
36 failureCount.incrementAndGet();
37 } finally {
38 latch.countDown();
39 }
40 });
41 }
42
43 // Wait for completion
44 latch.await(30, TimeUnit.SECONDS);
45 executor.shutdown();
46
47 // Assertions
48 assertEquals(1, successCount.get(), "Only one booking should succeed");
49 assertEquals(threadCount - 1, failureCount.get(), "Others should fail due to conflicts");
50 }
51
52 @Test
53 void testLockTimeoutBehavior() {
54 // Test pessimistic lock timeout scenarios
55 assertThrows(BookingLockException.class, () -> {
56 // Implementation details for timeout testing
57 });
58 }
59}
๐ฏ Conclusion and Recommendations
Data consistency in Java enterprise applications requires careful consideration of trade-offs between consistency, availability, and performance. The choice of pattern depends on your specific use case, but here are key recommendations:
๐ Summary of Patterns
- Optimistic Locking: Best for low-contention, read-heavy scenarios
- Pessimistic Locking: Ideal for high-contention, critical consistency needs
- Hybrid Strategy: Optimal for variable load patterns and adaptive systems
- Two-Phase Commit: Required for distributed ACID transactions
๐ Future Considerations
- Event Sourcing: For audit trails and temporal consistency
- CQRS: For read/write separation and eventual consistency
- Saga Pattern: Alternative to 2PC for long-running processes
- Reactive Patterns: For high-throughput, low-latency requirements
By implementing these patterns correctly and monitoring their performance, you can build robust Java applications that maintain data consistency while meeting performance requirements. Remember to always test thoroughly under realistic load conditions and have proper monitoring in place to detect and respond to consistency issues quickly.
The key is to start with the simplest approach that meets your consistency requirements and gradually adopt more sophisticated patterns as your system scales and requirements become more complex.