---
title: "Data modeling"
description: "Master Aerospike data modeling through access patterns, efficient batch reads, and optimal record sizing strategies."
---

# Data modeling

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

You drive Aerospike data modeling from access patterns, not from entity normalization or document embedding. This section explains the principles and patterns you need to design an effective data model on Aerospike.

## A different mental model

If you come from relational databases, your instinct is to normalize: one table per entity, small rows, joins at query time. If you come from document databases, your instinct is to embed: nest related data in a single document for atomic writes and single-read access.

Aerospike works differently. There are no joins and no server-side `$lookup`, and a [transaction](https://aerospike.com/docs/database/learn/transactions) spans records in one strong-consistency namespace only. Instead, Aerospike provides efficient [batch reads](https://aerospike.com/docs/develop/learn/batch/) that scatter/gather across nodes in parallel. These architectural differences, the [record size limit](https://aerospike.com/docs/develop/data-modeling/record-sizing) (1 MiB by default), the cost of [primary index](https://aerospike.com/docs/database/learn/architecture/data-storage/primary-index) metadata, and the efficiency of batch reads, give you a distinct set of modeling trade-offs.

The three approaches, compared:

| Approach | Optimizes for | Reassembly tool | Record shape |
| --- | --- | --- | --- |
| Relational | Storage efficiency (normalization) | Server-side joins | Small rows, many tables |
| Document | Read convenience (embedding) | Server-side `$lookup` or client round-trips | Large documents, few collections |
| Aerospike | Latency and memory cost | Batch reads (scatter/gather) | Medium records (1–128 KiB), shaped by access patterns |

## Access patterns drive design

Every Aerospike modeling decision follows from one question: _how does the application read and write this data?_

Records are the unit of I/O. A read fetches the whole record from storage, and a write rewrites the whole record atomically. Requesting a subset of bins reduces what crosses the network, not what the server reads from storage.

When the application needs data from multiple records, batch reads fetch them in parallel across the cluster. Designing your model so that common reads resolve to a single `get` or a bounded `batch get` is the primary goal.

This means:

-   **Consolidate** related data into a single record when it is read together and the combined size stays in the 1–128 KiB sweet spot.
-   **Split** data across multiple records when entities have different lifecycles, different access frequencies, or when consolidation would create records that are too large or too hot.
-   **Denormalize** when a copy of data in a second record serves a different access pattern and the application tolerates bounded staleness.

## Records, not rows

An Aerospike [record](https://aerospike.com/docs/database/learn/architecture/data-storage/data-model) is not a relational row. A record contains bins (named, typed fields), and each bin can hold a scalar value, a List, a Map, or a deeply nested combination of both. One bin holding a Map of 1,000 entries is often better than 1,000 records with one value each.

This flexibility means you can model a “user profile with 200 segment tags” as a single record with a Map bin, or a “sensor with a day’s readings” as a single record with a List bin — patterns that would require separate tables or embedded arrays in other systems.

The trade-off is that Aerospike has no server-enforced schema. The data model is an application-level contract: the team agrees on namespace, set, key format, bin names, and bin types so that all clients read and write the same logical structure.

## The cost of records

Every record in the cluster carries a fixed overhead: **64 bytes of [primary index](https://aerospike.com/docs/database/learn/architecture/data-storage/primary-index) metadata**, typically stored in memory. This metadata enables consistent, single-hop access to any record by key — it is what makes Aerospike fast.

But it also means that many tiny records cost disproportionate memory. A design with millions of 50-byte records spends more memory on index metadata than on data. Consolidating those into fewer, medium-sized records (each in the 1–128 KiB range) gives a much better index-to-data ratio while still allowing fine-grained access through [collection data type](https://aerospike.com/docs/develop/data-types/collections/) operations.

## Batch reads replace joins

In a relational database, you normalize and then join. In Aerospike, you design keys so that the data you need is either in one record or reachable through a bounded batch read.

The following example fetches a user profile and three recent order records in a single batch call. The application derives all four keys from values it already has — no index required.

-   [Java SDK](#tab-panel-3724)
-   [Python SDK](#tab-panel-3725)
-   [Rust](#tab-panel-3726)
-   [C#](#tab-panel-3727)
-   [Go](#tab-panel-3728)
-   [Node.js](#tab-panel-3729)
-   [C](#tab-panel-3730)
-   [Java](#tab-panel-3731)
-   [Python](#tab-panel-3732)

```java
DataSet users = DataSet.of("test", "users");

DataSet orders = DataSet.of("test", "orders");

try (RecordStream rs = session.query(

        users.id("user:alice"),

        orders.id("order:1001"),

        orders.id("order:1002"),

        orders.id("order:1003")).execute()) {

    rs.forEach(result -> System.out.println(result.recordOrThrow().bins));

}
```

```python
users = DataSet.of("test", "users")

orders = DataSet.of("test", "orders")

stream = session.query(

    users.id("user:alice"),

    orders.id("order:1001"),

    orders.id("order:1002"),

    orders.id("order:1003"),

).execute()

for row in stream:

    print(row.record_or_raise().bins)

stream.close()
```

```rust
let keys = [

    as_key!("test", "users", "user:alice"),

    as_key!("test", "orders", "order:1001"),

    as_key!("test", "orders", "order:1002"),

    as_key!("test", "orders", "order:1003"),

];

let brp = BatchReadPolicy::default();

let mut records: Vec<BatchOperation> = keys

    .into_iter()

    .map(|key| BatchOperation::read(&brp, key, Bins::All))

    .collect();

client.batch(&BatchPolicy::default(), &mut records).await?;

for op in &records {

    if let Some(rec) = op.record() {

        println!("{:?}", rec.bins);

    }

}
```

```csharp
Key[] keys = new Key[] {

    new Key("test", "users", "user:alice"),

    new Key("test", "orders", "order:1001"),

    new Key("test", "orders", "order:1002"),

    new Key("test", "orders", "order:1003")

};

Record[] records = client.Get(null, keys);

foreach (Record record in records)

{

    Console.WriteLine(record);

}
```

```go
// Requires: import as "github.com/aerospike/aerospike-client-go/v8"

key1, _ := as.NewKey("test", "users", "user:alice")

key2, _ := as.NewKey("test", "orders", "order:1001")

key3, _ := as.NewKey("test", "orders", "order:1002")

key4, _ := as.NewKey("test", "orders", "order:1003")

keys := []*as.Key{key1, key2, key3, key4}

records, err := client.BatchGet(nil, keys)

if err != nil {

  log.Fatal(err)

}

for _, r := range records {

  fmt.Println(r.Bins)

}
```

```js
const batchRecords = [

  { type: Aerospike.batchType.BATCH_READ, key: new Aerospike.Key('test', 'users', 'user:alice'), readAllBins: true },

  { type: Aerospike.batchType.BATCH_READ, key: new Aerospike.Key('test', 'orders', 'order:1001'), readAllBins: true },

  { type: Aerospike.batchType.BATCH_READ, key: new Aerospike.Key('test', 'orders', 'order:1002'), readAllBins: true },

  { type: Aerospike.batchType.BATCH_READ, key: new Aerospike.Key('test', 'orders', 'order:1003'), readAllBins: true },

]

const results = await client.batchRead(batchRecords)

for (const { record } of results) {

  console.log(record.bins)

}
```

```c
static bool batch_read_cb(const as_batch_result* results,

    uint32_t n, void* udata) {

    for (uint32_t i = 0; i < n; i++) {

        if (results[i].result != AEROSPIKE_OK) {

            continue;

        }

        as_record_iterator it;

        as_record_iterator_init(&it, &results[i].record);

        while (as_record_iterator_has_next(&it)) {

            as_bin* bin = as_record_iterator_next(&it);

            char* value = as_val_tostring(as_bin_get_value(bin));

            printf("%s=%s ", as_bin_get_name(bin), value);

            free(value);

        }

        as_record_iterator_destroy(&it);

        printf("\n");

    }

    return true;

}

as_batch batch;

as_batch_inita(&batch, 4);

as_key_init_str(as_batch_keyat(&batch, 0), "test", "users", "user:alice");

as_key_init_str(as_batch_keyat(&batch, 1), "test", "orders", "order:1001");

as_key_init_str(as_batch_keyat(&batch, 2), "test", "orders", "order:1002");

as_key_init_str(as_batch_keyat(&batch, 3), "test", "orders", "order:1003");

aerospike_batch_get(&as, &err, NULL, &batch, batch_read_cb, NULL);

as_batch_destroy(&batch);
```

```java
Key[] keys = new Key[] {

    new Key("test", "users", "user:alice"),

    new Key("test", "orders", "order:1001"),

    new Key("test", "orders", "order:1002"),

    new Key("test", "orders", "order:1003")

};

Record[] records = client.get(null, keys);

for (Record record : records) {

    System.out.println(record.bins);

}
```

```python
keys = [

    ("test", "users", "user:alice"),

    ("test", "orders", "order:1001"),

    ("test", "orders", "order:1002"),

    ("test", "orders", "order:1003"),

]

batch_records = client.batch_read(keys)

for rec in batch_records.batch_records:

    (_, _, bins) = rec.record

    print(bins)
```

Batch reads scatter the request across the nodes that own the relevant partitions and gather the results. For a batch of 10–100 keys, latency is typically close to a single-record read because the requests execute in parallel. This makes “many medium records, fetched together” a practical and performant pattern.

## What this section covers

The remaining pages in this section walk through the modeling principles and patterns in detail:

-   [Record sizing](https://aerospike.com/docs/develop/data-modeling/record-sizing): the 64-byte index cost, the 1–128 KiB sweet spot, and how to consolidate effectively.
-   [Key design](https://aerospike.com/docs/develop/data-modeling/key-design): how you design keys to control partition placement, access paths, and denormalization.
-   [Collections](https://aerospike.com/docs/develop/data-modeling/collections): List and Map patterns, nested operations, and list-of-structs with path expressions.
-   [Relationships](https://aerospike.com/docs/develop/data-modeling/relationships): 1:1, 1:N, and N:M patterns, write contention, and overflow strategies.
-   [Indexes](https://aerospike.com/docs/develop/data-modeling/indexes): secondary indexes, set indexes, expression indexes, and when not to index.
-   [Conventions](https://aerospike.com/docs/develop/data-modeling/conventions): bin naming, identifier formats, and timestamp contracts.

## Deeper design-time companion

The pages in this section explain Aerospike modeling principles and patterns for readers who already know what to build. When you need to _derive_ a data model from product requirements (greenfield or redesign), use the [Aerospike data modeling guide](https://github.com/aerospike/data-modeling-guide) on GitHub as the deeper companion. That workflow includes explicit stop points and workload and access-pattern worksheets. It produces two deliverables: a _schema guide_ (full design with reasoning) and a _schema summary_ (operational contract for implementers).

The guide is the authoritative source for material that does not yet have a dedicated page here, including _workload archetypes_ (representative record shapes and write amplification profiles) and _modeling failure modes_ (detection tests and corrective patterns). To install the Aerospike agent skill, which references that guide, see [AI coding agents](https://aerospike.com/docs/develop/ai-coding-agents).