Engineering Core
ISB Vietnam's skilled software engineers deliver high-quality applications, leveraging their extensive experience in developing financial tools, business management systems, medical technology, and mobile/web platforms.

Traditional offset-based pagination (using SKIP and TAKE) isn't viable in DynamoDB due to performance constraints. Instead, DynamoDB uses cursor-based pagination through LastEvaluatedKey, which acts as a pointer to the next page.

While navigating "Next" is straightforward, implementing a full set of controls—First, Previous, Next, and Last—requires a deeper understanding of DynamoDB's architecture.

In this guide, we’ll implement a complete pagination solution in C#.

Introduction to DynamoDB Pagination

Amazon DynamoDB is a fully managed NoSQL database designed for fast, scalable, and predictable performance. When querying large datasets, DynamoDB automatically paginates results and returns up to 1 MB of data per request.

Instead of using offset-based pagination like SQL, DynamoDB uses a special value called LastEvaluatedKey. Each query response includes:

  • A page of items.

  • A LastEvaluatedKey (if more items exist).

To retrieve the next page, the client passes this key back to DynamoDB using the ExclusiveStartKey parameter. Because DynamoDB does not support random access to pages, implementing controls like Previous and Last requires applying cursor logic or manipulating the sort order.

When to Use Pagination in DynamoDB

Pagination is essential when dealing with:

  • Large datasets: Fetching thousands of items in a single request is inefficient and costly.

  • User interfaces: UI components (dashboards, tables) need friendly controls.

  • APIs returning limited result sets: Public endpoints must paginate to avoid timeouts.

  • Reducing Read Costs: Controlled queries reduce Read Capacity Units (RCU) consumption.

  • High-traffic systems: Fetching data incrementally prevents backend resource exhaustion.

The Secret Weapon: ScanIndexForward

DynamoDB allows you to navigate forward easily. However, it does not natively support "Previous" or "Last". To solve this, we utilize the ScanIndexForward parameter.

  • ScanIndexForward = true (Default): Returns items in ascending order.

  • ScanIndexForward = false: Returns items in descending order.

This feature allows us to:

Efficiently get the "Last Page"

Querying in descending order gives you the newest/last items first.

Key Concept: The first page of a descending query is effectively the last page of an ascending query.

Support Backward Pagination

When moving backward, using reverse sort order allows us to fetch items preceding the current batch without scanning the entire table.

Note: This technique requires your Table or GSI to have a Sort Key defined.

Implementing Pagination (C# Example)

Below is a reusable pagination structure supporting First, Next, Previous, and Last.

Important: Handling State in Web APIs

Before looking at the code, note that in a stateless environment (like a REST API), you cannot store the PagingState object in server memory. You must serialize the state (e.g., to a Base64 JSON string) and send it to the client. The client must then send this token back in the next request.

Models

C#
using Amazon.DynamoDBv2.Model;

// Represents the result of a paginated query
public class PageResult
{
    // List of DynamoDB items returned for this page
    public List<Dictionary<string, AttributeValue>> Items { get; set; }

    // Cursor pointing to the next page (null if no more pages)
    public Dictionary<string, AttributeValue>? NextKey { get; set; }

    // Number of items per page
    public int PageSize { get; set; }
}

// Stores the pagination state for navigating Next/Previous
public class PagingState
{
    // Stack of previous page tokens -> used to move backwards
    // In a Web API, this list should be serialized and sent to the client
    public Stack<Dictionary<string, AttributeValue>?> PrevTokens { get; set; } = new();

    // Token used to load the current page
    public Dictionary<string, AttributeValue>? CurrentToken { get; set; }

    // Token used to load the next page
    public Dictionary<string, AttributeValue>? NextToken { get; set; }
}

Base Query Method

This generic method handles the core DynamoDB query logic.

C#
public async Task<PageResult> QueryPageAsync(
    string userType,
    Dictionary<string, AttributeValue>? startKey,
    int pageSize,
    bool scanForward = true)
{
    var request = new QueryRequest
    {
        TableName = "Users",
        // Partition key condition
        KeyConditionExpression = "UserType = :u",
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        {
            {":u", new AttributeValue { S = userType }}
        },
        // Cursor for next page (null for first page)
        ExclusiveStartKey = startKey,
        // Maximum items to return
        Limit = pageSize,
        // Sorting direction: true = ascending, false = descending
        ScanIndexForward = scanForward
    };

    var response = await _client.QueryAsync(request);

    return new PageResult
    {
         Items = response.Items,
         NextKey = response.LastEvaluatedKey,
         PageSize = pageSize
    };
}

First Page

C#
public async Task<PageResult> GetFirstPageAsync(string userType, int pageSize, PagingState state)
{
    // Clear backward history as we are starting over
    state.PrevTokens.Clear();
    state.CurrentToken = null;

    // Load first page in ascending order
    var result = await QueryPageAsync(userType, null, pageSize, scanForward: true);
    // Store next page cursor
    state.NextToken = result.NextKey;
    return result;
}

Next Page

C#
public async Task<PageResult> GetNextPageAsync(string userType, int pageSize, PagingState state)
{
    // Check if there are more pages
    if (state.NextToken == null)
        return new PageResult { Items = new(), NextKey = null };

    // Save current token to history so we can navigate backwards later
    state.PrevTokens.Push(state.CurrentToken);

    // Move forward
    state.CurrentToken = state.NextToken;
    // Load next page
    var result = await QueryPageAsync(userType, state.CurrentToken, pageSize);
    state.NextToken = result.NextKey;
    return result;
}

Previous Page

C#
public async Task<PageResult> GetPreviousPageAsync(string userType, int pageSize, PagingState state)
{
    // If no history, default to First Page
    if (!state.PrevTokens.Any())
        return await GetFirstPageAsync(userType, pageSize, state);

    // Retrieve the most recent previous token
    var previousKey = state.PrevTokens.Pop();
    // Update current cursor
    state.CurrentToken = previousKey;
    // Load page using the retrieved token
    var result = await QueryPageAsync(userType, previousKey, pageSize);
    state.NextToken = result.NextKey;
    return result;
}

Last Page

This is where the magic happens using ScanIndexForward = false.

C#
public async Task<PageResult> GetLastPageAsync(string userType, int pageSize)
{
    // Reverse the sort order so newest items come first
    // This effectively fetches the "Last Page" immediately
    var result = await QueryPageAsync(
        userType,
        startKey: null,
        pageSize: pageSize,
        scanForward: false); // Critical: Read backwards

    // Reorder items for UI display (so they appear Ascending within the page)
    result.Items.Reverse();

    return result;
}

Note on Navigation: Jumping directly to the "Last Page" isolates the user from the previous navigation history. The PrevTokens stack will not automatically know how to go back to the "Second to Last" page. In most UI implementations, clicking "Last" resets the navigation context.

Conclusion

DynamoDB’s cursor-based pagination offers a scalable and cost-efficient alternative to offset-based pagination. While paging forward is simple, paging backward and jumping to the last page requires creative use of sorting.

By leveraging ScanIndexForward = false, developers can:

  1. Retrieve the last page instantly (O(1) complexity).

  2. Reverse the paging direction efficiently.

  3. Reduce unnecessary read costs.

With the C# implementation provided, you now have a robust starting point for building user-friendly tables on top of DynamoDB.

References

These authoritative resources help deepen your understanding:

 

Ready to get started?

Contact IVC for a free consultation and discover how we can help your business grow online.

Contact IVC for a Free Consultation
Written by
Author Avatar
Engineering Core
ISB Vietnam's skilled software engineers deliver high-quality applications, leveraging their extensive experience in developing financial tools, business management systems, medical technology, and mobile/web platforms.

COMPANY PROFILE

Please check out our Company Profile.

Download

COMPANY PORTFOLIO

Explore my work!

Download

ASK ISB Vietnam ABOUT DEVELOPMENT

Let's talk about your project!

Contact US