Skip to content

Dynamic Keys with SpEL

Garvit Joshi edited this page Mar 20, 2026 · 7 revisions

Dynamic Keys with SpEL

Locksmith supports Spring Expression Language (SpEL) for creating dynamic lock keys based on method parameters.

Important: SpEL Syntax

SpEL expressions MUST be wrapped in #{...} syntax:

// Correct - SpEL expression
@DistributedLock(key = "#{#userId}")
public void updateUser(String userId) { }

// Incorrect - treated as literal string "#userId"
@DistributedLock(key = "#userId")
public void updateUser(String userId) { }

Literal keys can contain # character:

// These are all literal keys (not SpEL)
@DistributedLock(key = "order#123")       // Literal
@DistributedLock(key = "task#end")        // Literal
@DistributedLock(key = "#userId")         // Literal (no #{} wrapper)

SpEL vs Literal Keys

Key Expression Type Resolved Value
"#{#userId}" SpEL Value of userId parameter
"#userId" Literal String "#userId"
"order#123" Literal String "order#123"
"#{'user-' + #id}" SpEL Concatenated string like "user-42"
"#{#user.name}" SpEL Value of user.name property

Why Dynamic Keys?

Static keys lock globally:

// All users blocked while any user's order is processing
@DistributedLock(key = "process-order")
public void processOrder(String userId, String orderId) { }

Dynamic keys enable fine-grained locking:

// Only the specific order is locked
@DistributedLock(key = "#{'order-' + #orderId}")
public void processOrder(String userId, String orderId) { }

Basic Parameter Reference

Use #{#parameterName} to reference method parameters:

@DistributedLock(key = "#{#userId}")
public void updateUser(String userId) {
    // Lock key: lock:user123
}

@DistributedLock(key = "#{#orderId}")
public void processOrder(Long orderId) {
    // Lock key: lock:12345
}

String Concatenation

Combine literals and parameters with +:

@DistributedLock(key = "#{'user-' + #userId}")
public void updateUser(String userId) {
    // Lock key: lock:user-user123
}

@DistributedLock(key = "#{'order-' + #orderId + '-process'}")
public void processOrder(Long orderId) {
    // Lock key: lock:order-12345-process
}

Note: String literals in SpEL use single quotes: 'literal'

Object Properties

Access object properties with dot notation:

@DistributedLock(key = "#{#user.id}")
public void updateUser(User user) {
    // Lock key: lock:123
}

@DistributedLock(key = "#{#order.customer.id}")
public void processOrder(Order order) {
    // Lock key: lock:456
}

@DistributedLock(key = "#{'user-' + #user.email}")
public void sendEmail(User user) {
    // Lock key: lock:user-john@example.com
}

Multiple Parameters

Combine multiple parameters:

@DistributedLock(key = "#{#tenantId + '-' + #userId}")
public void updateUserInTenant(String tenantId, String userId) {
    // Lock key: lock:tenant1-user123
}

@DistributedLock(key = "#{'transfer-' + #fromAccount + '-to-' + #toAccount}")
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
    // Lock key: lock:transfer-ACC001-to-ACC002
}

Parameter by Position

Access parameters by position using #p0, #p1, etc. or #a0, #a1, etc.:

@DistributedLock(key = "#{#p0}")  // First parameter
public void processUser(String userId) {
    // Lock key: lock:user123
}

@DistributedLock(key = "#{#a0 + '-' + #a1}")  // First and second parameters
public void process(String first, String second) {
    // Lock key: lock:first-second
}

Method Calls

Call methods on parameters:

@DistributedLock(key = "#{#email.toLowerCase()}")
public void processEmail(String email) {
    // Lock key: lock:john@example.com (normalized)
}

@DistributedLock(key = "#{#list.size()}")
public void processList(List<String> list) {
    // Lock key: lock:5
}

@DistributedLock(key = "#{#date.toLocalDate().toString()}")
public void processForDate(LocalDateTime date) {
    // Lock key: lock:2024-01-15
}

@DistributedLock(key = "#{#user.getId()}")
public void processUser(User user) {
    // Lock key: lock:123
}

Conditional Expressions

Use ternary operator for conditional keys:

@DistributedLock(key = "#{#premium ? 'premium-' + #userId : 'standard-' + #userId}")
public void processUser(String userId, boolean premium) {
    // premium=true:  lock:premium-user123
    // premium=false: lock:standard-user123
}

@DistributedLock(key = "#{#amount > 1000 ? 'large' : 'small'}")
public void processPayment(double amount) {
    // amount=1500: lock:large
    // amount=500:  lock:small
}

Null Safety

Handle potentially null values:

// Using Elvis operator
@DistributedLock(key = "#{#user.nickname ?: #user.id}")
public void updateUser(User user) {
    // Uses nickname if present, otherwise id
}

// Using safe navigation
@DistributedLock(key = "#{#order?.customer?.id ?: 'anonymous'}")
public void processOrder(Order order) {
    // Returns 'anonymous' if order, customer, or id is null
}

Array and Collection Access

Access elements by index:

@DistributedLock(key = "#{#ids[0]}")
public void processFirst(List<String> ids) {
    // Lock key: lock:first-id
}

@DistributedLock(key = "#{#args[0] + '-' + #args[1]}")
public void process(String... args) {
    // Lock key: lock:arg1-arg2
}

@DistributedLock(key = "#{#users[0].name}")
public void processFirstUser(List<User> users) {
    // Lock key: lock:John
}

Map Access

Access map values:

@DistributedLock(key = "#{#params['orderId']}")
public void process(Map<String, String> params) {
    // Lock key: lock:order123
}

@DistributedLock(key = "#{#context['tenant'] + '-' + #context['user']}")
public void processInContext(Map<String, Object> context) {
    // Lock key: lock:tenant1-user123
}

Static Methods and Constants

Reference static members using T() syntax:

@DistributedLock(key = "#{T(java.util.UUID).randomUUID().toString()}")
public void uniqueLock() {
    // Lock key: lock:550e8400-e29b-41d4-a716-446655440000
    // Warning: This creates a new lock every time!
}

@DistributedLock(key = "#{'version-' + T(com.example.AppVersion).CURRENT}")
public void migrate() {
    // Lock key: lock:version-2.0.0
}

@DistributedLock(key = "#{T(java.lang.String).valueOf(#orderId)}")
public void processOrder(Long orderId) {
    // Lock key: lock:12345
}

@DistributedLock(key = "#{T(java.lang.Math).max(#a, #b)}")
public void processMax(int a, int b) {
    // Lock key: lock:100 (if max is 100)
}

Common Patterns

Per-User Locking / Rate Limiting

@DistributedLock(key = "#{'user-profile-' + #userId}")
public void updateProfile(String userId, Profile profile) { }

@DistributedLock(key = "#{'user-settings-' + #user.id}")
public void updateSettings(User user, Settings settings) { }

@RateLimit(key = "#{'user-' + #userId}", permits = 100, interval = "1m")
public void processUserRequest(String userId) { }

Per-Resource Locking

@DistributedLock(key = "#{'document-' + #documentId}")
public void editDocument(String documentId, String content) { }

@DistributedLock(key = "#{'file-' + #path.hashCode()}")
public void writeFile(Path path, byte[] data) { }

Per-Tenant Locking

@DistributedLock(key = "#{#tenantId + ':user:' + #userId}")
public void updateTenantUser(String tenantId, String userId) {
    // Lock key: lock:tenant1:user:user123
}

Composite Keys

@DistributedLock(key = "#{'inventory-' + #warehouseId + '-' + #productId}")
public void updateInventory(String warehouseId, String productId, int quantity) {
    // Lock key: lock:inventory-WH001-PROD123
}

Date-Based Locking

@DistributedLock(key = "#{'daily-report-' + T(java.time.LocalDate).now().toString()}")
public void generateDailyReport() {
    // Lock key: lock:daily-report-2024-01-15
}

@DistributedLock(key = "#{'hourly-' + #instant.truncatedTo(T(java.time.temporal.ChronoUnit).HOURS)}")
public void hourlyTask(Instant instant) { }

Literal Keys with # Character

Since SpEL requires #{...} wrapper, you can use # in literal keys without any issues:

// All these are literal keys (not SpEL)
@DistributedLock(key = "order#123")
public void processOrder() {
    // Lock key: lock:order#123
}

@DistributedLock(key = "task#end")
public void endTask() {
    // Lock key: lock:task#end
}

@DistributedLock(key = "item-#1")
public void processItem() {
    // Lock key: lock:item-#1
}

@DistributedLock(key = "prefix#middle#suffix")
public void process() {
    // Lock key: lock:prefix#middle#suffix
}

Validation

Locksmith validates SpEL expressions at runtime:

Null Result

@DistributedLock(key = "#{#user.id}")
public void process(User user) { }

// If user.id is null:
// IllegalArgumentException: SpEL expression '#user.id' evaluated to null

Blank Result

@DistributedLock(key = "#{#name}")
public void process(String name) { }

// If name is "" or "   ":
// IllegalArgumentException: SpEL expression '#name' evaluated to blank string

Invalid Expression

@DistributedLock(key = "#{#nonexistent}")
public void process(String value) { }

// If parameter doesn't exist:
// SpelEvaluationException: Property or field 'nonexistent' cannot be found

Best Practices

1. Always Use #{...} for SpEL

// Correct - SpEL will be evaluated
@DistributedLock(key = "#{#userId}")

// Incorrect - treated as literal "#userId"
@DistributedLock(key = "#userId")

// Correct - literal key (no evaluation needed)
@DistributedLock(key = "static-key")

2. Keep Keys Short but Meaningful

// Good
@DistributedLock(key = "#{'ord-' + #orderId}")

// Verbose but acceptable
@DistributedLock(key = "#{'order-processing-' + #orderId}")

// Too long - wastes Redis memory
@DistributedLock(key = "#{'order-processing-workflow-step-1-' + #orderId}")

3. Normalize Keys

// Normalize email to prevent duplicates
@DistributedLock(key = "#{'email-' + #email.toLowerCase().trim()}")
public void processEmail(String email) { }

4. Use Prefixes for Clarity

@DistributedLock(key = "#{'read:' + #docId}")
public Document readDocument(String docId) { }

@DistributedLock(key = "#{'write:' + #docId}")
public void writeDocument(String docId, Document doc) { }

5. Avoid Non-Deterministic Expressions

// Bad - different key each time
@DistributedLock(key = "#{T(java.util.UUID).randomUUID()}")

// Bad - time-dependent
@DistributedLock(key = "#{T(System).currentTimeMillis()}")

// Good - deterministic based on input
@DistributedLock(key = "#{#userId}")

Migration Guide

If you're upgrading from an earlier version that didn't require #{...} wrapper:

Before (old syntax):

@DistributedLock(key = "#userId")
@DistributedLock(key = "'user-' + #id")
@DistributedLock(key = "#user.name")

After (new syntax):

@DistributedLock(key = "#{#userId}")
@DistributedLock(key = "#{'user-' + #id}")
@DistributedLock(key = "#{#user.name}")

Benefits of new syntax:

  • Literal keys can contain # character (e.g., order#123)
  • Clear distinction between SpEL and literal keys
  • Consistent with Spring's property placeholder syntax
  • No ambiguity or special escaping needed

Test Examples

For comprehensive examples demonstrating all SpEL features documented on this page, see the test suite:

WikiSpELExamplesTest.java

This test file contains working examples of:

  • Basic parameter references (#{#userId}, #{#orderId})
  • String concatenation (#{'user-' + #id})
  • Object property access (#{#user.name}, #{#order.customer.id})
  • Multiple parameter combinations (#{#tenantId + '-' + #userId})
  • Parameter access by position (#{#p0}, #{#a0})
  • Method invocations (#{#email.toLowerCase()}, #{#list.size()})
  • Conditional expressions (#{#premium ? 'premium-' + #userId : 'standard-' + #userId})
  • Null safety operators (#{#user.nickname ?: #user.id}, #{#order?.customer?.id})
  • Collection and array access (#{#users[0].name})
  • Map access (#{#params['orderId']})
  • Static method calls (#{T(java.lang.String).valueOf(#id)})
  • Common patterns (per-user, per-resource, per-tenant locking)
  • Normalized keys (#{#email.toLowerCase().trim()})

Each example in the test suite validates that the SpEL expression correctly resolves to the expected lock key.

Next Steps

Clone this wiki locally