Server applications usually rely on a single primary database. When an application or microservice operates only on that database, maintaining transactional consistency is relatively simple. The problem begins when the application also needs to send a message to a queue or save data in another microservice. In such a case, the external service is outside the control of the local transaction.
This is a significant issue in distributed systems, which is why the industry has developed best practices for maintaining data integrity.
Example scenario:
we want to save data in a table and send a message to a queue informing that the operation has completed successfully. This is a very common business case. Instead of a queue, it could be any external system — the mechanism remains the same.
How can this problem be solved to avoid data inconsistencies? One of the most popular approaches is the Transactional Outbox pattern.
Example implementation in Java Spring:
This solution guarantees that if the database transaction fails, no message will be sent to the queue. As a result, it prevents data inconsistencies and reduces the need for complex error handling. In practice, this saves both time and resources — financial as well as human.
A major challenge in web applications is managing concurrent modifications of database records. If multiple users save unrelated data into the same table, there is usually no issue. However, systems should always be designed with conflict scenarios in mind.
Example case:
We have a record in the meetings table. Two users read the same data at the same time. Then both attempt to modify the name of the same record. What happens? Usually, the last saved change overwrites the previous one.
To prevent this, locking mechanisms are used.
The optimistic approach assumes that conflicts are rare. Data is not locked during reading, and conflicts are detected only during saving.
In Spring, this is typically implemented by adding a field annotated with @Version in the entity. During an update, the framework checks the record version. If another user modified the record in the meantime, an exception is thrown indicating a conflict.
Example:
@Version
private Long version;
The pessimistic approach assumes that conflicts may occur from the very beginning. The record is locked during reading, preventing other users from modifying it simultaneously.
Example in Spring:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(“select a from Data a where a.id = :id”)
Optional findByIdForUpdate(@Param(“id”) Long id);
In this case, another user must wait until the lock is released. This approach increases data safety but may reduce system performance under high concurrency.