Skip to content

Programmatic Templates

Garvit Joshi edited this page Jan 28, 2026 · 2 revisions

Programmatic Templates

While annotations provide a declarative approach, sometimes you need programmatic control over lock, semaphore, and rate limit operations. Locksmith provides template classes for these scenarios.

When to Use Templates

Use the programmatic API when:

  • Lock/semaphore/rate limit keys are determined at runtime in complex ways
  • You need fine-grained control over acquisition and release timing
  • Annotations don't fit your use case (e.g., non-method boundaries)
  • You want to conditionally acquire locks or check rate limits based on runtime state

LocksmithLockTemplate

The LocksmithLockTemplate provides programmatic access to distributed locks.

Injection

Both templates are auto-configured and can be injected:

@Service
public class MyService {

    private final LocksmithLockTemplate lockTemplate;

    public MyService(LocksmithLockTemplate lockTemplate) {
        this.lockTemplate = lockTemplate;
    }
}

Simple Usage

Try-Finally Pattern

if (lockTemplate.tryLock("my-resource")) {
    try {
        // Critical section - only one instance executes this
        processResource();
    } finally {
        lockTemplate.unlock("my-resource");
    }
} else {
    // Lock not acquired - handle accordingly
    log.info("Resource is busy");
}

Callback Pattern (Recommended)

The callback pattern ensures the lock is always released, even if an exception occurs:

String result = lockTemplate.executeWithLock("my-resource", () -> {
    // Critical section
    return processResource();
});

if (result == null) {
    // Lock was not acquired
    log.info("Could not acquire lock");
}

Check Lock Status

boolean isLocked = lockTemplate.isLocked("my-resource");

Builder API for Advanced Options

Use the builder when you need custom timing, lock types, or auto-renewal:

Custom Wait and Lease Time

boolean acquired = lockTemplate.forKey("my-resource")
    .waitTime(Duration.ofSeconds(5))    // Wait up to 5 seconds
    .leaseTime(Duration.ofMinutes(2))   // Hold for up to 2 minutes
    .tryLock();

Lock Types

// Read lock - multiple readers allowed
lockTemplate.forKey("shared-data")
    .lockType(LockType.READ)
    .execute(() -> readData());

// Write lock - exclusive access
lockTemplate.forKey("shared-data")
    .lockType(LockType.WRITE)
    .execute(() -> writeData());

Auto-Renewal

For long-running operations where you can't predict the duration:

String result = lockTemplate.forKey("long-running-job")
    .autoRenew()  // Lock will be automatically renewed while held
    .execute(() -> {
        // Long-running operation - lock won't expire
        return processLargeDataset();
    });

Full Builder Example

boolean acquired = lockTemplate.forKey("my-resource")
    .waitTime(Duration.ofSeconds(10))
    .leaseTime(Duration.ofMinutes(5))
    .lockType(LockType.WRITE)
    .tryLock();

if (acquired) {
    try {
        // Do work
    } finally {
        lockTemplate.forKey("my-resource")
            .lockType(LockType.WRITE)
            .unlock();
    }
}

LocksmithSemaphoreTemplate

The LocksmithSemaphoreTemplate provides programmatic access to distributed semaphores.

Injection

@Service
public class ApiService {

    private final LocksmithSemaphoreTemplate semaphoreTemplate;

    public ApiService(LocksmithSemaphoreTemplate semaphoreTemplate) {
        this.semaphoreTemplate = semaphoreTemplate;
    }
}

Simple Usage

Try-Finally Pattern

// Limit to 5 concurrent executions across all instances
String permitId = semaphoreTemplate.tryAcquirePermit("api-calls", 5);
if (permitId != null) {
    try {
        // At most 5 instances execute this simultaneously
        callExternalApi();
    } finally {
        semaphoreTemplate.releasePermit("api-calls", permitId);
    }
} else {
    // No permit available
    log.info("API rate limit reached");
}

Callback Pattern (Recommended)

String result = semaphoreTemplate.executeWithPermit("api-calls", 5, () -> {
    // At most 5 concurrent executions
    return callExternalApi();
});

if (result == null) {
    // Permit was not acquired
    log.info("Rate limit reached");
}

Builder API for Advanced Options

Custom Wait and Lease Time

String permitId = semaphoreTemplate.forKey("api-calls", 5)
    .waitTime(Duration.ofSeconds(30))   // Wait up to 30 seconds for a permit
    .leaseTime(Duration.ofMinutes(1))   // Permit expires after 1 minute
    .tryAcquire();

Execute with Custom Timing

String result = semaphoreTemplate.forKey("api-calls", 5)
    .waitTime(Duration.ofSeconds(10))
    .leaseTime(Duration.ofMinutes(2))
    .execute(() -> {
        return callExternalApi();
    });

LocksmithRateLimitTemplate

The LocksmithRateLimitTemplate provides programmatic access to distributed rate limiters.

Injection

@Service
public class ApiService {

    private final LocksmithRateLimitTemplate rateLimitTemplate;

    public ApiService(LocksmithRateLimitTemplate rateLimitTemplate) {
        this.rateLimitTemplate = rateLimitTemplate;
    }
}

Simple Usage

Try-Acquire Pattern

// Check if rate limit allows the request (10 permits per second by default)
if (rateLimitTemplate.tryAcquire("api-endpoint")) {
    // Within rate limit
    processRequest();
} else {
    // Rate limit exceeded
    log.info("Rate limit exceeded");
}

Callback Pattern (Recommended)

The callback pattern provides cleaner code:

String result = rateLimitTemplate.executeWithRateLimit("api-endpoint", () -> {
    // Rate limited operation
    return processRequest();
});

if (result == null) {
    // Rate limit was exceeded
    log.info("Rate limited");
}

Builder API for Advanced Options

Use the builder when you need custom permits, intervals, or rate types:

Custom Permits and Interval

boolean acquired = rateLimitTemplate.forKey("api-endpoint")
    .permits(100)                        // 100 requests
    .interval(Duration.ofMinutes(1))     // per minute
    .tryAcquire();

Rate Types

// Overall - shared across all instances (default)
rateLimitTemplate.forKey("global-api")
    .permits(100)
    .interval(Duration.ofMinutes(1))
    .rateType(RateType.OVERALL)
    .execute(() -> callApi());

// Per-client - each instance gets its own quota
rateLimitTemplate.forKey("instance-api")
    .permits(100)
    .interval(Duration.ofMinutes(1))
    .rateType(RateType.PER_CLIENT)
    .execute(() -> callApi());

Wait for Permit

boolean acquired = rateLimitTemplate.forKey("api-endpoint")
    .permits(100)
    .interval(Duration.ofMinutes(1))
    .waitTime(Duration.ofSeconds(5))  // Wait up to 5 seconds for a permit
    .tryAcquire();

Full Builder Example

String result = rateLimitTemplate.forKey("user-api")
    .permits(100)
    .interval(Duration.ofMinutes(1))
    .rateType(RateType.OVERALL)
    .waitTime(Duration.ofSeconds(5))
    .execute(() -> {
        return processUserRequest();
    });

Key Prefixes

All templates automatically apply the configured key prefixes. For example:

locksmith:
  lock:
    key-prefix: "myapp:lock:"
  semaphore:
    key-prefix: "myapp:sem:"
  rate-limit:
    key-prefix: "myapp:rl:"
  • lockTemplate.tryLock("orders") → Redis key: myapp:lock:orders
  • semaphoreTemplate.tryAcquirePermit("pool", 5) → Redis key: myapp:sem:pool
  • rateLimitTemplate.tryAcquire("api") → Redis key: myapp:rl:api

Thread Safety

All templates are thread-safe and can be safely shared across threads. They are also compatible with virtual threads (Project Loom).

Comparison: Annotations vs Templates

Feature Annotations Templates
Declarative Yes No
Dynamic keys at runtime Via SpEL Full control
Fine-grained timing control Limited Full control
Non-method boundaries No Yes
Automatic resource cleanup Yes With callback pattern
Code readability High Medium

When to Use Each

Use annotations (@DistributedLock, @DistributedSemaphore, @RateLimit) for:

  • Method-level locking/rate limiting with known keys
  • Simple SpEL expressions for dynamic keys
  • Clean, declarative code

Use templates (LocksmithLockTemplate, LocksmithSemaphoreTemplate, LocksmithRateLimitTemplate) for:

  • Complex key generation logic
  • Conditional locking/rate limiting based on runtime state
  • Non-method boundaries (e.g., within loops, across method calls)
  • Maximum control over timing and behavior

See Also

Clone this wiki locally