---
title: "Hash-Partitioned AttributeMap"
description: "Use this pattern when records have a stable lookup key and each record contains structured data. The sample stores customer profiles by email address. A lookup reads one partition, not the entire directory."
canonical_url: "https://docs.superdurable.io/design-patterns/hash-partitioned-attribute-map/"
source: "docs/content/design-patterns/hash-partitioned-attribute-map.mdx"
locale: "en"
language: "python"
---

> Index of every page for AI agents: https://docs.superdurable.io/llms.txt

# Hash-Partitioned AttributeMap

Code on this page is in Python. Other languages: [Go](https://docs.superdurable.io/design-patterns/hash-partitioned-attribute-map.go.md) · [Java](https://docs.superdurable.io/design-patterns/hash-partitioned-attribute-map.java.md) · [TypeScript](https://docs.superdurable.io/design-patterns/hash-partitioned-attribute-map.typescript.md) · [Rust](https://docs.superdurable.io/design-patterns/hash-partitioned-attribute-map.rust.md)

Use this pattern when records have a stable lookup key and each record contains structured data. The sample stores customer profiles by email address. A lookup reads one partition, not the entire directory.

`CustomerDirectoryFlow` is an RPC-only Flow. Start it once with no Flow timeout. The `CustomerProfilesByEmailPartition` AttributeMap has 1,000 possible instances, and each one exists only once it is needed. Each instance stores a dictionary from canonical email to the full profile.

## Stable partition algorithm

Every SDK uses the same algorithm:

1. Require a non-empty ASCII email address.
2. Remove leading and trailing ASCII whitespace.
3. Lowercase ASCII letters from A through Z.
4. Calculate 32-bit FNV-1a over the canonical email bytes with wrapping overflow.
5. Select **hash modulo 1000** and format the instance from **partition-000** through **partition-999**.

A hash collision only places several emails in one partition. The dictionary still tells the records apart by their complete canonical email.

## Exact lookup and serialized writes

The controller and the RPC handler each calculate the partition. When the controller calls `UpsertCustomerProfile`, it asks Dex to load that one partition instance and to lock it. The handler calculates the partition again, reads the bucket, updates one profile, and writes the bucket back. Writes to the same partition run one at a time. Writes to different partitions can run concurrently.

Concurrent writes to one partition can receive a lock-conflict error. The upsert is idempotent, so retry it; see [RPC lock conflicts](https://docs.superdurable.io/reference/server/runtime-data.md#rpc-lock-conflicts). To coordinate writes across the whole map, see [whole-map coordination](https://docs.superdurable.io/reference/server/runtime-data.md#whole-map-coordination).

`GetCustomerProfileByEmail` loads one partition without a write lock. A missing partition and a missing canonical email both return not found.

**Python**

```python
saved_profile = await app_state.client.invoke_rpc(
    flow.upsert_customer_profile,
    FLOW_ID,
    profile,
    options=RPCInvokeOptions(
        lock_attribute_map_instances=(
            flow.customer_profiles_by_email_partition.lock(partition_name),
        ),
        load_attribute_map_instances=(
            flow.customer_profiles_by_email_partition.load(partition_name),
        ),
    ),
)
```

Example: [examples/python/dex_examples/patterns/hash-partitioned-attribute-map/controller.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/patterns/hash-partitioned-attribute-map/controller.py)

Every writer must lock the partition instance it writes. A lock only holds back other writers that take the same lock. A writer that skips the lock can still overwrite their changes.

## HTTP API

```text
POST /patterns/hash-partitioned-attribute-map/start
PUT  /patterns/hash-partitioned-attribute-map/customer-profile
GET  /patterns/hash-partitioned-attribute-map/customer-profile?emailAddress=alice@example.com
```

The partition count is part of the persisted data. Changing it moves most keys to other partitions, so it requires a complete rehash or a new Flow version. The sample does not implement deletion, full-directory enumeration, cross-field search, or partition resizing.

## Next

[Entity Flow with SQL Projection](https://docs.superdurable.io/design-patterns/entity-store.md): Keep one Flow for each entity, such as a user.
