ShedLock doesn’t actually prevent multiple instances from executing a scheduled task; it ensures only one instance acquires the lock for that task.

Let’s see ShedLock in action. Imagine you have a @Scheduled task that sends out a daily digest email. Without ShedLock, if you have two instances of your Spring Boot app running, both could potentially send the same email.

@Configuration
@EnableScheduling
@EnableSchedulerLock // Crucial: enables ShedLock
public class ShedLockConfig {
    // ... ShedLock configuration beans go here
}

@Component
public class EmailScheduler {

    private static final Logger log = LoggerFactory.getLogger(EmailScheduler.class);

    @Scheduled(cron = "0 0 9 * * *") // Runs daily at 9 AM
    @SchedulerLock(name = "sendDailyDigest", lockAtMostFor = "PT1H") // Lock for 1 hour
    public void sendDailyDigest() {
        log.info("Attempting to send daily digest...");
        // Simulate email sending logic
        try {
            Thread.sleep(5000); // Simulate work
            log.info("Daily digest email sent successfully.");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("Email sending interrupted.", e);
        }
    }
}

Here, @EnableSchedulerLock is the key annotation that activates ShedLock. The @SchedulerLock annotation on sendDailyDigest specifies a unique name for the task ("sendDailyDigest") and lockAtMostFor, which defines how long the lock will be held. PT1H is ISO 8601 duration format for "1 hour".

The problem ShedLock solves is straightforward: ensuring idempotency for scheduled tasks across a distributed system. If you have a task that must run exactly once within a given period, and you have multiple instances of your application that could potentially pick it up, you need a mechanism to coordinate. ShedLock provides this coordination by leveraging an external, shared data store (like a database or Redis) to manage lock ownership.

Internally, when a scheduled task annotated with @SchedulerLock is about to execute, ShedLock attempts to acquire a lock in the configured data store. This lock is identified by the name provided in the annotation. If the lock is successfully acquired, the task proceeds. If another instance already holds the lock, the current instance’s task execution is skipped for that interval. The lockAtMostFor parameter is a safeguard: if an instance holding a lock crashes, the lock will eventually expire, allowing another instance to pick up the task.

The primary lever you control is the configuration of the ShedLock LockProvider. This tells ShedLock where to store and check for locks. Common providers include:

  • JDBC: Uses a database table. Requires a table with columns like lock_name and lock_until.
  • Redis: Uses Redis keys. Fast and efficient.
  • Zookeeper: Uses Zookeeper ephemeral nodes.
  • Mongo: Uses a MongoDB collection.

You configure this in your Spring Boot application, typically via application.properties or application.yml. For example, with JDBC:

shedlock.provider=jdbc
shedlock.jdbc.table-name=SHEDLOCK_LOCKS

Or with Redis:

shedlock.provider=redis
shedlock.redis.connection=redis://localhost:6379

The lockAtMostFor duration is critical. If it’s too short, a long-running task might lose its lock and be re-executed by another instance. If it’s too long, a crashed instance could prevent the task from running for an extended period. The value should be longer than the expected maximum execution time of your task.

What most people miss is how lockAtLeastFor interacts with lockAtMostFor. While lockAtMostFor is a hard expiry, lockAtLeastFor ensures a lock is held for a minimum duration, even if the task finishes early. This is useful to prevent "thundering herd" scenarios where a task finishes very quickly, and multiple instances immediately try to re-acquire the lock in rapid succession, potentially leading to contention. If you have lockAtLeastFor = "PT5M" and lockAtMostFor = "PT1H", the lock will be held for at least 5 minutes and at most 1 hour.

The next concept you’ll grapple with is handling task failures gracefully within a distributed locking context, ensuring retries or dead-letter queues are managed correctly without violating the "run once" guarantee.

Want structured learning?

Take the full Spring-boot course →