Hash-Partitioned AttributeMap
View as MarkdownUse 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:
- Require a non-empty ASCII email address.
- Remove leading and trailing ASCII whitespace.
- Lowercase ASCII letters from A through Z.
- Calculate 32-bit FNV-1a over the canonical email bytes with wrapping overflow.
- 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. To coordinate writes across the whole map, see whole-map coordination.
GetCustomerProfileByEmail loads one partition without a write lock. A missing partition and a missing canonical email both return not found.
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
err = controller.client.InvokeRPCWithOptions(
request.Request.Context(),
FlowID,
controller.flow.UpsertCustomerProfile,
profile,
&savedProfile,
dex.RPCInvokeOptions{
LockAttributeMapInstances: []dex.AttributeLock{
dex.LockAttributeMap(CustomerProfilesByEmailPartition, partitionName),
},
LoadAttributeMapInstances: []dex.AttributeMapLoad{
CustomerProfilesByEmailPartition.Load(partitionName),
},
},
)
Example: examples/go/patterns/hash-partitioned-attribute-map/controller.go
final RPCInvokeOptions options = RPCInvokeOptions.newBuilder()
.addLockAttributeMapInstance(
flow.customerProfilesByEmailPartition,
partition.partitionName)
.addLoadAttributeMapInstance(
flow.customerProfilesByEmailPartition,
partition.partitionName)
.build();
return ResponseEntity.ok(client.invokeRPC(
stub::upsertCustomerProfile,
profile,
options));
await client.invokeRPCWithOptions(
customerDirectoryFlow.upsertCustomerProfile,
CUSTOMER_DIRECTORY_FLOW_ID,
profile,
{
lockAttributeMapInstances: [profiles.lock(partitionName)],
loadAttributeMapInstances: [profiles.load(partitionName)],
},
)
Example: examples/typescript/src/patterns/hash-partitioned-attribute-map/controller.ts
client.invoke_rpc_with_options(
FLOW_ID,
UPSERT_CUSTOMER_PROFILE,
profile,
RpcInvokeOptions::new()
.lock_attribute_map_instance(
CUSTOMER_PROFILES_BY_EMAIL_PARTITION.lock(&partition.partition_name),
)
.load_attribute_map_instance(
CUSTOMER_PROFILES_BY_EMAIL_PARTITION.load(partition.partition_name),
),
)
Example: examples/rust/src/patterns/hash_partitioned_attribute_map/controller.rs
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
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: Keep one Flow for each entity, such as a user.