Microservices15 min read·By Liyabona Saki·

Implementing Distributed Locking with Java Spring Boot and Redis for Data Integrity

A production guide to distributed locking with Spring Boot and Redis (Redisson) — preventing race conditions, ensuring idempotency, and handling failure gracefully in high-concurrency systems.

Advertisement

Introduction

In a single-JVM application, synchronized or a ReentrantLock is enough. Run two replicas behind a load balancer and those locks protect nothing — each JVM has its own. Distributed locking coordinates mutual exclusion across processes, machines and regions.

The most common implementation: Redis + Redisson.

What distributed locking solves

  • Race conditions — two replicas processing the same message simultaneously.
  • Duplicate processing — a retried webhook charging the customer twice.
  • Resource contention — only one node should run the nightly export.
  • Idempotency — guarantee an operation runs at most once per key.

A concrete race condition

java
@PostMapping("/orders/{id}/refund")
public void refund(@PathVariable String id) {
  Order o = orders.findById(id);
  if (o.getStatus() == REFUNDED) return;
  paymentGateway.refund(o);          // ← if two requests get here simultaneously
  o.setStatus(REFUNDED);              //   the customer is refunded twice
  orders.save(o);
}

The "check then act" between line 3 and line 4 is the race.

Step 1 — Add Redisson

xml
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.32.0</version>
</dependency>
yaml
spring:
  redis:
    host: redis
    port: 6379
redisson:
  single-server-config:
    address: redis://redis:6379

Step 2 — Lock the critical section

```java
@Service
public class RefundService {
  private final RedissonClient redisson;
  private final OrderRepository orders;
  private final PaymentGateway gateway;

public RefundService(RedissonClient r, OrderRepository o, PaymentGateway g) { this.redisson = r; this.orders = o; this.gateway = g; }

public void refund(String orderId) { RLock lock = redisson.getLock("order:" + orderId + ":refund"); boolean acquired = false; try { // wait up to 2s, hold for at most 10s acquired = lock.tryLock(2, 10, TimeUnit.SECONDS); if (!acquired) throw new ConcurrentOperationException("refund already in progress");

Order o = orders.findById(orderId).orElseThrow(); if (o.getStatus() == OrderStatus.REFUNDED) return; gateway.refund(o); o.setStatus(OrderStatus.REFUNDED); orders.save(o); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } finally { if (acquired && lock.isHeldByCurrentThread()) lock.unlock(); } } } ```

Three things are non-negotiable:

1. Bounded wait time — never call lock() without a timeout, or you'll exhaust threads. 2. Lease time — Redis auto-expires the lock if the JVM crashes mid-section. 3. Release in finally + check ownership — never release a lock you don't own.

Step 3 — How Redis distributed locks actually work

Redisson's lock is a Lua script around SET key value NX PX <ttl>:

text
SET order:42:refund <uuid> NX PX 10000
  • NX — only set if not exists (atomic acquisition).
  • PX 10000 — auto-expire after 10s (dead-holder safety).
  • value = client UUID — release script checks ownership before deleting.

The watchdog thread renews the TTL while the JVM is alive, so a healthy holder never loses the lock.

Step 4 — Sequence diagram

text
client A ──► refund(42) ──► SET NX (OK)        ──► gateway.refund ──► UNLOCK
client B ──► refund(42) ──► SET NX (FAIL, wait)
                                  └─► retry ──► SET NX (FAIL, status=REFUNDED) ──► return

Step 5 — Idempotency keys (a better default for HTTP)

For external-facing endpoints, prefer idempotency keys stored as a Redis SETNX with TTL:

java
String key = "idemp:" + request.getHeader("Idempotency-Key");
Boolean first = redis.opsForValue().setIfAbsent(key, "in-progress", Duration.ofHours(24));
if (Boolean.FALSE.equals(first)) return cachedResponse(key);

Stripe and most payment APIs work this way. Combine with a distributed lock when the *processing* of the key needs mutual exclusion.

Step 6 — Docker Compose for local Redis

yaml
services:
  redis:
    image: redis:7
    ports: ["6379:6379"]
    command: ["redis-server","--appendonly","yes"]
  app:
    build: .
    environment: [SPRING_REDIS_HOST=redis]
    depends_on: [redis]

Real-world examples

  • Payments — preventing duplicate captures or refunds.
  • Banking transfers — locking both accounts in a deterministic order to avoid deadlock.
  • Inventory — decrementing stock for a hot product during a flash sale.
  • Scheduled jobs — only one replica should run the 02:00 export.
  • Cache regeneration — single-flight rebuilds of an expensive cache key.

Production best practices

  • Always set a lease time. A lock without TTL is a future outage.
  • Keep critical sections short. Network calls inside a lock multiply contention; pre-compute, then lock only the mutation.
  • Use logical, stable keysorder:<id>:refund, not random UUIDs.
  • Monitor lock wait/hold times. Surface them as Micrometer metrics.
  • Use Redisson's RedLock across multiple independent Redis nodes only if you need correctness against full-node failure — it's stricter and slower than the single-node lock.

Common pitfalls and anti-patterns

  • Holding a lock across HTTP calls to a slow external API — one slow call can block every replica.
  • Using SETNX without TTL — JVM crash leaves a permanent lock.
  • Locking with @Transactional that commits *after* unlock — another node sees stale data. Acquire → load → mutate → commit → unlock, in that order.
  • Assuming Redis fail-over preserves locks — async replication can lose the lock during a failover window. Combine with idempotency for safety.

Related tutorials

Architecture

Distributed Locking with Redis

INSTANCESLOCKRESOURCEacquirewaitwaitexclusive writeApp Instance AApp Instance BApp Instance CRedis LockSETNX + TTLShared ResourceDB row / job
Multiple instances coordinate access to a shared resource via a Redis SETNX lock with TTL, preventing concurrent execution of a critical section.

TL;DR

Key takeaways

  • Understand the core concepts behind Implementing Distributed Locking with Java Spring Boot and Redis for Data Integrity in a production context.
  • Apply the patterns to real Microservices systems, not just toy examples.
  • Recognize the trade-offs, failure modes, and operational concerns before adopting them.
  • Get a clear path to the next step — related tutorials, tools, and reference architectures.

Avoid these

Common mistakes

  • 1. Copy-pasting code without understanding the trade-offs

    It's tempting to ship a snippet from a blog post into production, but Microservices patterns only work when the failure modes are understood. Always reason about timeouts, retries, and consistency.

  • 2. Skipping observability from day one

    Structured logs, metrics, and traces are not optional. Wire them in before you ship — debugging Microservices systems without them is painful and expensive.

  • 3. Optimizing too early

    Premature caching, sharding, or microservice extraction adds operational cost. Validate the bottleneck with real measurements first.

  • 4. Ignoring security defaults

    Secrets in env files, open management ports, missing RBAC — these are the most common production incidents. Treat security as part of the definition of done.

Ship it safely

Production best practices

Apply these before promoting Implementing Distributed Locking with Java Spring Boot and Redis for Data Integrity to a real production environment.

Scalability

Design Microservices services to scale horizontally. Keep request handlers stateless, push session and cache state to external stores (Redis, the database), and benchmark p95/p99 latency under realistic load before tuning.

Monitoring & Observability

Emit metrics (RED/USE), structured JSON logs, and distributed traces from day one. Wire dashboards and alerts to SLOs you actually care about — error rate, latency, saturation — not vanity metrics.

Logging

Log with correlation IDs, never log secrets or PII, and centralize logs (ELK, Loki, CloudWatch). Use levels deliberately: INFO for state changes, WARN for recoverable issues, ERROR for incidents.

Security

Apply least-privilege IAM, rotate secrets through a vault, validate every input, and patch dependencies on a schedule. For HTTP services, enable TLS everywhere and set sensible security headers.

Testing

Layer unit, integration, and contract tests. Run them in CI on every PR, and add smoke tests post-deploy. For Microservices systems, also run chaos and load tests before a major release.

Reliability & Rollouts

Ship with health checks, readiness probes, graceful shutdown, and a rollback strategy. Prefer canary or blue/green deploys over big-bang releases.

Questions

Frequently asked questions

Is this tutorial up to date?

Yes. This tutorial was last reviewed and updated on May 24, 2026. We revisit popular Microservices tutorials regularly to keep them aligned with current best practices.

What level is this tutorial aimed at?

It is written for working developers with some backend experience. Beginners can still follow along, and senior engineers will find production-grade patterns and trade-off discussions.

Do I need to follow every step in order?

The walkthrough is sequential because each step depends on the previous one. If you only need a specific concept, the table of contents at the top of the article lets you jump straight to that section.

Where can I find the source code?

Code samples are inlined in the tutorial. When a companion repository is published it will be linked at the top of this page.

Go deeper

Further reading

#Redis#Redisson#Spring Boot#Distributed Systems#Concurrency#Idempotency

More From the Channel

Follow the full tutorial series on YouTube

The MasterLabSystems channel publishes in-depth, project-based tutorials on Java, Spring Boot, microservices, Docker, Kubernetes, AWS and DevOps — the same topics covered on this site, with full code walkthroughs.

Stay in the Loop

Get the next tutorial in your inbox

next tutorial →

Modular Monolith Architecture in Spring Boot — The Right Way to Scale a Monolith

Related tutorials