> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0021-activity-stream-cursor-pagination.md).

# ADR-0021: Activity streams use cursor pagination

## Status

**Accepted.** Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.

## Context

The activity feed is an append-only audit trail that can grow very large. Two pagination shapes are available: offset/limit (`page` + `size`), which the platform uses for aggregate listings like alerts, and cursor pagination, which carries a pointer to the last item seen. Offset paging degrades as the offset grows — the database must scan and discard every skipped row — which is exactly the wrong cost profile for deep scrolling through a long audit log.

## Decision

**Activity-stream endpoints paginate by a `(lastEventId, lastEventDateTime)` cursor, not offset/limit.** The caller passes the id and timestamp of the last event it saw, plus a `size`; the next page is the rows immediately after that cursor. The same cursor shape is reused across the platform-wide activity feed (`getActivity`) and the per-data-entity activity feed (`getDataEntityActivityList`), establishing cursor pagination as the convention for activity streams specifically.

This is **not** a global pagination convention: aggregate/mutable listings (alerts, the data-entity list) continue to use offset/limit (`page` + `size`). The pagination shape follows the data semantics — append-only audit streams get cursors; bounded mutable listings get offsets.

## Consequences

* Deep scrolling through the activity feed stays cheap: each page is an indexed seek past the cursor rather than an offset scan, so cost does not grow with how far back the reader has paged.
* The cursor is a compound key `(lastEventId, lastEventDateTime)` because event timestamps are not unique — the id breaks ties so the page boundary is deterministic.
* A reader cannot jump to "page N" of the activity feed (there are no page numbers) — only forward/continued paging from a cursor. That is the accepted trade-off of the shape, and it matches how an audit feed is actually read.
* There is a known naming wart: the controller-level cursor parameter is spelled `lasEventId` (missing a "t"), so the generated client exposes that misspelling on the public contract even though the service-layer parameter is the correct `lastEventId`. The decision is the cursor *shape*; the misspelled parameter name is a contract blemish that renaming would be a breaking change to fix.

## Evidence

* `odd-platform-api/.../controller/ActivityController.java:34-35` — `getActivity` takes `final Long lasEventId, final OffsetDateTime lastEventDateTime` (the cursor pair; note the `lasEventId` misspelling at the controller/contract layer) and passes them to the service.
* `odd-platform-api/.../service/activity/ActivityServiceImpl.java:96-97` — `getActivityList(... final Long lastEventId, final OffsetDateTime lastEventDateTime)`; `:179-180` — `fetchAllActivities` forwards them to `activityRepository.findAllActivities(... lastEventId, lastEventDateTime)`.
* `odd-platform-api/.../service/activity/ActivityServiceImpl.java:119-127` — `getDataEntityActivityList` takes the same `(lastEventId, lastEventDateTime)` cursor pair, confirming the shape is reused across activity-stream endpoints.

## See also

* [Activity Feed](/features/active-platform-features/activity-feed.md) — the user-facing activity stream this paginates.
* [ADR-0022 — Activity view-modes are a single enum parameter](/developer-guides/architecture-decision-log/adr-0022-activity-view-modes-single-parameter.md) — the companion API-shape decision for the same endpoint.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0021-activity-stream-cursor-pagination.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
