Interface OutboxRecordRepository

All Superinterfaces:
org.springframework.data.repository.CrudRepository<OutboxRecord,OutboxRecord.PK>, org.springframework.data.jpa.repository.JpaRepository<OutboxRecord,OutboxRecord.PK>, org.springframework.data.repository.ListCrudRepository<OutboxRecord,OutboxRecord.PK>, org.springframework.data.repository.ListPagingAndSortingRepository<OutboxRecord,OutboxRecord.PK>, org.springframework.data.repository.PagingAndSortingRepository<OutboxRecord,OutboxRecord.PK>, org.springframework.data.repository.query.QueryByExampleExecutor<OutboxRecord>, org.springframework.data.repository.Repository<OutboxRecord,OutboxRecord.PK>

public interface OutboxRecordRepository extends org.springframework.data.jpa.repository.JpaRepository<OutboxRecord,OutboxRecord.PK>
Spring Data repository for OutboxRecord.

The default JpaRepository surface covers CRUD against the compound PK OutboxRecord.PK; the additional poller-specific query methods declared here drive the PENDING → SENT transition implemented in OutboxPollerWorker (ADR-0014 D-7).

All non-bulk methods are read-only by default; the Modifying methods (markSent(java.util.UUID, java.util.UUID, java.time.Instant), markFailureAttempt(java.util.UUID, java.util.UUID, java.lang.String, int)) MUST be invoked inside an active transaction — they're configured to clear the persistence context via flushAutomatically=true, clearAutomatically=true so downstream reads see the up-to-date row state.

  • Method Summary

    Modifier and Type
    Method
    Description
    long
    Pending-total gauge support.
    org.springframework.data.domain.Page<OutboxRecord>
    findByStatusOrderByCreatedAtAsc(OutboxRecord.Status status, org.springframework.data.domain.Pageable pageable)
    Pageable PENDING-row fetch for the poller's cold-path sweep.
    Backlog-age gauge support.
    int
    markFailureAttempt(UUID aggregateId, UUID eventId, String lastError, int maxRetries)
    Atomic retry-bump on publish failure.
    int
    markSent(UUID aggregateId, UUID eventId, Instant sentAt)
    Atomic PENDING → SENT transition for a single row keyed by the compound PK.

    Methods inherited from interface org.springframework.data.repository.CrudRepository

    count, delete, deleteAll, deleteAll, deleteAllById, deleteById, existsById, findById, save

    Methods inherited from interface org.springframework.data.jpa.repository.JpaRepository

    deleteAllByIdInBatch, deleteAllInBatch, deleteAllInBatch, deleteInBatch, findAll, findAll, flush, getById, getOne, getReferenceById, saveAllAndFlush, saveAndFlush

    Methods inherited from interface org.springframework.data.repository.ListCrudRepository

    findAll, findAllById, saveAll

    Methods inherited from interface org.springframework.data.repository.ListPagingAndSortingRepository

    findAll

    Methods inherited from interface org.springframework.data.repository.PagingAndSortingRepository

    findAll

    Methods inherited from interface org.springframework.data.repository.query.QueryByExampleExecutor

    count, exists, findAll, findBy, findOne
  • Method Details

    • findByStatusOrderByCreatedAtAsc

      org.springframework.data.domain.Page<OutboxRecord> findByStatusOrderByCreatedAtAsc(OutboxRecord.Status status, org.springframework.data.domain.Pageable pageable)
      Pageable PENDING-row fetch for the poller's cold-path sweep. Ordering by createdAt ASC ensures FIFO replay behaviour, which combined with the (status, created_at) compound index makes the scan O(N) in batch size, not table size.
      Parameters:
      status - the target status (typically OutboxRecord.Status.PENDING)
      pageable - bounded page; the poller passes a page of OutboxProperties.poller.batch-size per tick
      Returns:
      a page of rows in FIFO creation order
    • findOldestCreatedAtByStatus

      @Query("SELECT MIN(o.createdAt) FROM OutboxRecord o WHERE o.status = :status") Optional<Instant> findOldestCreatedAtByStatus(@Param("status") OutboxRecord.Status status)
      Backlog-age gauge support. Returns the createdAt of the oldest row in the given status (typically PENDING); the metrics binder subtracts this from now to expose im2be_outbox_backlog_age_seconds.
      Parameters:
      status - the target status
      Returns:
      the oldest createdAt, or empty when no rows match
    • countByStatus

      long countByStatus(OutboxRecord.Status status)
      Pending-total gauge support. Total row count for a given status — sourced via JPQL count so the query planner uses the (status, created_at) index for an index-only scan.
      Parameters:
      status - the target status
      Returns:
      total number of rows in that status
    • markSent

      @Modifying(flushAutomatically=true, clearAutomatically=true) @Query("UPDATE OutboxRecord o SET o.status = com.aim2be.platform.outbox.OutboxRecord.Status.SENT, o.sentAt = :sentAt, o.lastError = NULL WHERE o.aggregateId = :aggregateId AND o.eventId = :eventId AND o.status = com.aim2be.platform.outbox.OutboxRecord.Status.PENDING") int markSent(@Param("aggregateId") UUID aggregateId, @Param("eventId") UUID eventId, @Param("sentAt") Instant sentAt)
      Atomic PENDING → SENT transition for a single row keyed by the compound PK. Clears lastError as part of the transition (a successful publish supersedes any prior failure context).

      Invoked by both the hot AFTER_COMMIT relay (via OutboxPublisher.tryHotPublish) and the cold poller (via OutboxPollerWorker). The WHERE … status = PENDING guard makes the transition idempotent under concurrent paths — a delayed hot-relay callback that fires AFTER the cold poller has already transitioned the row to OutboxRecord.Status.FAILED (or vice versa) MUST NOT corrupt the terminal state. The rowcount=0 return value signals the no-op to the caller (R3 finding — concurrent terminal-state corruption).

      R4 fix — the target status is hard-coded to OutboxRecord.Status.SENT inside the JPQL. The previous newStatus parameter exposed a footgun via the public repository API: a caller passing OutboxRecord.Status.FAILED would persist a non-null sentAt on a never-delivered row. Distinct lifecycle transitions belong on distinct repository methods.

      Parameters:
      aggregateId - compound-PK component
      eventId - compound-PK component
      sentAt - wall-clock instant at which the publish succeeded
      Returns:
      number of rows updated (1 on applied transition; 0 when the row was already in a terminal status — caller logs at DEBUG)
    • markFailureAttempt

      @Modifying(flushAutomatically=true, clearAutomatically=true) @Query("UPDATE OutboxRecord o SET o.retries = o.retries + 1, o.lastError = :lastError, o.status = CASE WHEN o.retries + 1 >= :maxRetries THEN com.aim2be.platform.outbox.OutboxRecord.Status.FAILED ELSE com.aim2be.platform.outbox.OutboxRecord.Status.PENDING END WHERE o.aggregateId = :aggregateId AND o.eventId = :eventId AND o.status = com.aim2be.platform.outbox.OutboxRecord.Status.PENDING") int markFailureAttempt(@Param("aggregateId") UUID aggregateId, @Param("eventId") UUID eventId, @Param("lastError") String lastError, @Param("maxRetries") int maxRetries)
      Atomic retry-bump on publish failure. Increments the row's retries counter, stores the (truncated) error message, AND conditionally transitions the row to OutboxRecord.Status.FAILED when the post-increment retries count reaches the configured maxRetries budget — all in a single JPQL UPDATE.

      R5 fix — the previous shape (Java-side budget check on the in-memory batch snapshot) raced with concurrent hot-relay failures that could push DB retries past maxRetries between the poller's batch fetch and the failure-outcome write. Evaluating the budget INSIDE the UPDATE statement uses the database's CURRENT retries value at write time, eliminating the snapshot race.

      Guarded on status = PENDING so a late hot-relay failure callback that fires AFTER the cold poller has already transitioned the row to OutboxRecord.Status.FAILED (or a parallel hot path has already transitioned it to OutboxRecord.Status.SENT) cannot mutate the terminal row (R3 finding — concurrent terminal-state corruption). The rowcount=0 return value signals the no-op to the caller.

      Parameters:
      aggregateId - compound-PK component
      eventId - compound-PK component
      lastError - stringified exception summary; the caller is expected to truncate to OutboxRecord.LAST_ERROR_MAX_LENGTH characters before invocation
      maxRetries - retry budget from im2be.outbox.poller.max-retries; when retries + 1 >= maxRetries the row also transitions to OutboxRecord.Status.FAILED in the same UPDATE statement
      Returns:
      number of rows updated (1 on applied increment; 0 when the row was already in a terminal status — caller logs at DEBUG)