What does @Tailable do on a reactive repository method, and what are its requirements?
answer
- Tailable cursor = tail -f
- Requires capped collection
- Must return Flux, never Mono
- Inserts only, no updates/deletes
- Errors (not completes) on dead cursor; add retry
basics
~10 s@Tailable makes a Flux query use a tailable cursor: it stays open and keeps emitting new matching documents as they are inserted, like tail -f. It requires a capped collection and must return Flux.
solid answer
~40 s@Tailable marks a reactive repository (or template) query as a tailable cursor. Instead of returning results and completing, the cursor stays open and the returned Flux emits each newly inserted document that matches, indefinitely, until you cancel the subscription or the collection is dropped. Requirements: the target must be a capped collection (fixed-size, insertion-ordered) and the method must return Flux, never Mono. It only sees inserts, not updates or deletes. It is useful for simple pub/sub-style streaming of an append-only log to WebFlux clients (for example via Server-Sent Events). Because it is infinite you must handle cancellation, and if the cursor dies (collection emptied or connection lost) the Flux errors, so production code usually adds retry/resubscription. For richer needs (updates, deletes, resume) use change streams instead.
code
java · 22 lines@Document
// collection must be created capped, e.g. at startup:
// template.createCollection("events",
// CollectionOptions.empty().capped().size(1_000_000).maxDocuments(10_000));
record Event(@Id String id, String streamId, Instant at, String payload) {}
interface EventRepository extends ReactiveMongoRepository<Event, String> {
@Tailable
Flux<Event> findByStreamId(String streamId);
}
@RestController
class EventStreamController {
private final EventRepository repo;
EventStreamController(EventRepository repo) { this.repo = repo; }
@GetMapping(value = "/streams/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Event> stream(@PathVariable String id) {
return repo.findByStreamId(id)
.retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)));
}
}go deeper
Know it means a continuous tail -f style stream of new documents.
State the capped-collection and Flux requirements and insert-only visibility.
Handle dead-cursor errors with retry and reason about eviction/gap risks.
Decide tailable vs change streams at an architecture level given durability, resumability, and collection-shape constraints.
**`@Tailable`** (`org.springframework.data.mongodb.repository.Tailable`) turns an ordinary reactive finder into one backed by a MongoDB **tailable cursor**. **What a tailable cursor is.** Normally a `find` cursor returns the matching batch and closes. A **tailable** cursor behaves like `tail -f` on a file: after returning current matches it **stays open**, and whenever a new document is appended that matches the query, the server pushes it and the client emits it. This maps perfectly onto a reactive **`Flux`** that never completes on its own. **Hard requirements.** 1. **Capped collection.** Tailable cursors only work on **capped collections** — fixed-size, insertion-ordered ring buffers created with `createCollection(name, CollectionOptions.empty().capped().size(bytes).maxDocuments(n))`. On a normal collection the cursor is rejected. 2. **Return type `Flux<T>`.** The method must return `Flux`; a `Mono` makes no sense for an unbounded stream and is rejected. 3. **Insert-only visibility.** The cursor sees **new inserts** only. It does **not** observe updates or deletes to existing documents. (Capped collections forbid document-growing updates and forbid deletes anyway.) **How to declare it.** ``` interface EventRepository extends ReactiveMongoRepository<Event, String> { @Tailable Flux<Event> findByStreamId(String streamId); } ``` Or via the template: `reactiveMongoTemplate.tail(query, Event.class)`. **Lifecycle / cancellation.** Because the `Flux` is infinite, the producer runs until the **subscriber cancels** (e.g., the WebFlux client disconnects, cancelling the SSE subscription) or the cursor becomes invalid. When streaming to clients you typically expose it as `text/event-stream` and let backpressure and cancellation flow from the HTTP connection. **Failure modes / gotchas.** - **CursorNotFound / dead cursor.** If the capped collection is emptied, dropped, or the connection drops, the tailable `Flux` terminates with an **error**, not a normal completion. Robust code adds `.retryWhen(...)` to re-open the cursor. - **Empty collection at start.** A tailable cursor over an empty capped collection can return immediately/behave oddly; MongoDB recommends the collection be non-empty (or use `awaitData`, which Spring's tailable support enables) so it waits for data. - **No resume token.** Unlike change streams, a tailable cursor has no resume position; after a failure you re-tail from the current end and may miss documents inserted during the gap. - **Capped size eviction.** Because the collection is a ring buffer, old documents are overwritten once the cap is hit; slow consumers can miss evicted entries. **When to use.** Lightweight, insert-only streaming of an append-only feed (audit log, chat-style stream) where a capped collection is acceptable and occasional gaps are tolerable. When you need updates/deletes, resumability, or streaming from normal collections, use **change streams** (`ReactiveMongoTemplate.changeStream(...)` / `@ChangeStream`-style listeners) instead.
- Why must a @Tailable method return Flux and not Mono?A tailable cursor is an unbounded, continuously-emitting stream with no natural single result. Mono models 0..1 and would complete after one element, so Spring requires Flux for the infinite stream.
- What is a key limitation of tailable cursors versus change streams?Tailable cursors work only on capped collections, see inserts only, and have no resume token, so after a failure they cannot resume from a known position and may miss data. Change streams work on any collection, observe insert/update/delete/replace, and support resume tokens.
saying these in an interview costs you the question
- Saying @Tailable works on any (non-capped) collection
- Expecting it to emit updates or deletes
- Assuming the Flux completes normally when the cursor dies (it errors)
- Returning Mono from a tailable method