Where should custom data live in Znode 10?
Choose between entity attributes, Custom Tables, and project-owned relational storage by examining ownership, relationships, and concurrent writes.
In this guide
The field that became a workflow
Imagine a request to store an external service reference against an account. One field sounds simple. A month later the reference has a status, several contacts, an approval history, and two people editing it at once. The original implementation now needs rules that its storage choice never expressed.
Begin by describing the information before choosing a table. Who owns it? Is it a property of an existing Znode entity, an independent mapping, or a business transaction with its own lifecycle? Those questions tell you more than the number of fields. A single status field can carry more operational responsibility than a large descriptive record.
Three useful starting points
An entity attribute is a candidate when the value belongs to one existing entity and should travel through its supported administration and API lifecycle. An external reference on an account fits that shape. Confirm how values, locales, groups, and validation are handled by the target implementation.
A Custom Table can suit a small, bounded lookup or mapping that does not naturally belong to an entity. Project-owned relational storage becomes more attractive when you need relationships, transactions, history, concurrency control, or frequent writes. Choose the project's storage and connectivity model explicitly. Access Znode-owned data through supported APIs.
Parse a fixed Custom Table result
Place ServiceMappingRow.cs in the Custom API's NativeDataExtensions area. Its Parse method classifies absence, duplicates, malformed fields, and mismatched account/store scope. The C# class accepts a successful response already mapped to three known fields; transport failures should propagate through the API error boundary.
The inspected 10.11 client exposes ICustomTableDataClient.GetRecordsByTableKeyAsync. In the infrastructure reader, use the fixed table key, a MappingKey equality filter with FilterOperators.Is, page one, and a two-row limit. Map the returned field rows into the dictionaries accepted here. Keep that SDK-specific envelope mapping in the reader so the application's validation stays stable when package contracts change.
NativeDataExtensions/ServiceMappingRow.cs csharp
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Gcg.DeveloperExamples.Data
{
public sealed class ServiceMappingRow
{
public int StoreId { get; }
public int AccountId { get; }
public string ExternalReference { get; }
private ServiceMappingRow(int storeId, int accountId, string externalReference)
{ StoreId = storeId; AccountId = accountId; ExternalReference = externalReference; }
public static ServiceMappingRow Parse(
IReadOnlyList<IReadOnlyDictionary<string, string>> rows,
int authorizedStoreId, int authorizedAccountId)
{
if (authorizedStoreId < 1 || authorizedAccountId < 1)
throw new UnauthorizedAccessException();
if (rows.Count == 0) return null;
if (rows.Count != 1) throw new InvalidOperationException("Duplicate mapping key.");
var row = rows[0];
string storeText, accountText, reference;
int storeId, accountId;
if (!row.TryGetValue("StoreId", out storeText) ||
!row.TryGetValue("AccountId", out accountText) ||
!row.TryGetValue("ExternalReference", out reference) ||
!int.TryParse(storeText, NumberStyles.None, CultureInfo.InvariantCulture, out storeId) ||
!int.TryParse(accountText, NumberStyles.None, CultureInfo.InvariantCulture, out accountId) ||
string.IsNullOrWhiteSpace(reference) || reference.Length > 100)
throw new InvalidOperationException("Invalid mapping data.");
if (storeId != authorizedStoreId || accountId != authorizedAccountId)
throw new UnauthorizedAccessException();
return new ServiceMappingRow(storeId, accountId, reference);
}
}
}
// Map the fixed Custom Table response's known fields into these dictionaries.
// Keep transport errors distinct from a successful response containing zero rows.
Decide what a conflicting edit means
A note edited by two operators needs a different contract from a static mapping. Give the editor a version token with its read, then require that token when saving. The update should succeed only if the stored version still matches. Otherwise return a conflict and preserve the user's proposed text for review.
Do not implement this as a separate version read followed by an unconditional update. Another writer can change the row between those operations. The comparison and update must be atomic in the chosen persistence layer. If the supported storage contract cannot provide the guarantee your workflow needs, reconsider the storage boundary rather than disguising the race in application code.
Verify the unpleasant cases first
Create fictional accounts in two stores with deliberately different mappings. Read them through authenticated contexts, then attempt a cross-account read and a mismatched stored row. Add a duplicate mapping, remove a required field, and simulate an unavailable dependency. Each should have a deliberate outcome that does not become an unrelated account's fallback.
For writable relational data, make two editors read the same version and save different values. Exactly one conditional update should succeed for that version. Verify that history and the current value remain consistent after rollback. Keep the conflicting-editor scenario in the persistence adapter's regression suite.
- Preserve existing entity values when adding owned settings.
- Keep credentials out of attributes and public responses.
- Record schema and adapter versions with the release.
Write the ownership decision down
Finish with a short decision record: owner, expected query shape, write frequency, uniqueness rule, authorization scope, and recovery process. Include why the alternatives were rejected. This gives the next developer a reason to preserve the boundary instead of moving data simply because another endpoint is convenient.
Revisit the decision when the lifecycle changes. A mapping may remain a mapping for years. A record that becomes an approval queue deserves another review. The useful design is the one whose guarantees match the business behavior, including failure, rather than the one that stores the first sample fastest.
References and further reading
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.