import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.Instant;
import java.util.UUID;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, UUID> {
@Modifying
@Query("delete from RefreshToken t where t.expiresAt < :now")
int deleteAllByExpiresAtBefore(@Param("now") Instant now);
}
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(
name = "refresh_tokens",
indexes = @Index(name = "idx_refresh_tokens_expires_at", columnList = "expires_at")
)
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "token", nullable = false, unique = true, length = 128)
private String token;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "expires_at", nullable = false)
private Instant expiresAt;
protected RefreshToken() {
}
public RefreshToken(String token, Long userId, Instant expiresAt) {
this.token = token;
this.userId = userId;
this.expiresAt = expiresAt;
}
public boolean isExpired() {
return expiresAt.isBefore(Instant.now());
}
public UUID getId() {
return id;
}
public Instant getExpiresAt() {
return expiresAt;
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
@Component
public class TokenCleanupJob {
private static final Logger log = LoggerFactory.getLogger(TokenCleanupJob.class);
private final RefreshTokenRepository refreshTokenRepository;
public TokenCleanupJob(RefreshTokenRepository refreshTokenRepository) {
this.refreshTokenRepository = refreshTokenRepository;
}
@Scheduled(cron = "0 0 3 * * *", zone = "UTC")
@Transactional
public void cleanupExpiredTokens() {
Instant cutoff = Instant.now();
int removed = refreshTokenRepository.deleteAllByExpiresAtBefore(cutoff);
if (removed > 0) {
log.info("Nightly cleanup removed {} expired refresh tokens (cutoff={})", removed, cutoff);
} else {
log.debug("Nightly cleanup found no expired refresh tokens (cutoff={})", cutoff);
}
}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
This snippet shows the common housekeeping problem of pruning stale rows on a schedule: refresh tokens that have passed their expiry date accumulate forever unless something removes them, bloating the table and slowing lookups. The pattern is a single background task that runs once a night and issues one bulk DELETE instead of loading and removing entities one by one.
In RefreshToken entity the row carries an expiresAt timestamp and an Instant-based isExpired() helper. The unique token column and index on expiresAt matter here — the cleanup query filters on expiresAt, so an index keeps the nightly scan cheap even as the table grows. Storing timestamps as Instant avoids time-zone ambiguity, which is important when the delete boundary is Instant.now().
RefreshTokenRepository declares deleteAllByExpiresAtBefore as a modifying bulk query. The @Modifying annotation tells Spring Data this is a write, not a SELECT, and @Query with delete from RefreshToken t where t.expiresAt < :now compiles to a single JPQL bulk delete. This bypasses the persistence context entirely — no entities are hydrated into memory — which is exactly what is wanted for a mass purge. The method returns the affected row count so the job can log how much it removed.
TokenCleanupJob wires it together. @Scheduled(cron = "0 0 3 * * *") fires at 03:00 every day; the zone attribute pins the schedule to UTC so it behaves identically across servers. The cleanupExpiredTokens method is annotated @Transactional because a bulk @Modifying query must run inside a transaction, and it passes Instant.now() as the cutoff. The trade-off of a bulk delete is that it does not fire JPA lifecycle callbacks or cascade through the persistence context, so it should only be used where those side effects are unnecessary — which is true for expired tokens.
A pitfall worth noting: @Scheduled runs on a small default thread pool, and a huge delete could block other scheduled tasks, so very large tables may warrant batching or a dedicated executor. Scheduling must also be enabled once via @EnableScheduling, shown in SchedulingConfig. This approach fits any periodic purge — sessions, one-time codes, audit rows — where a simple cron plus one indexed delete is far cheaper than a full ORM sweep.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
Share this code
Here's the card — post it anywhere.