A billing export once had enough information to reconstruct a missing account, but not enough to name it.
The account still existed in an older system. Its stable identifier reached the export action. No matching row existed in the newer application’s user table, so the action built a temporary object with safe fallback details. That object had a synthetic local identifier, but its export identifier remained null. Later, a queued data object required that identifier to be a string and rejected the payload.
The fallback solved missing attributes. It had not preserved identity.
One entity can have several legitimate identifiers
Applications tend to call a field id long after that name has stopped being
precise. An account shared across two systems might have all of these:
external_account_id:
durable identity understood by both systems
local_user_id:
primary key of one row in one database
public_id:
identifier safe to expose in URLs
correlation_id:
identifier for one request or trace
idempotency_key:
identifier for one attempted effectThey are not interchangeable. A local row key answers where this representation is stored. The external account identifier answers which account the message is about. A correlation identifier groups execution evidence. An idempotency key recognizes repeated work.
The names matter because the lifetimes differ. Delete and recreate a local row, and its primary key may change. Replay the same export after that change, and the account it describes must not.
The boundary decides which identity must survive
Inside one database, a generated primary key is usually an excellent reference. It is compact, immutable, and enforced by foreign keys. Trouble begins when its meaning escapes the boundary which owns it.
Suppose an export job contains only this:
{
"local_user_id": 4821
}The worker now depends on that exact row remaining available. Archival, a partial import, database replacement, or a race between deletion and execution can make the message meaningless.
A durable cross-system message needs the identity understood at that boundary:
{
"external_account_id": "acct_demo_7f3",
"local_user_id": 4821
}The local key can still help with lookup and diagnosis. It cannot be the only way to determine the subject when the receiver does not share the same table.
This does not mean one global identifier should replace every database key. Local keys belong to local persistence. Durable identifiers belong in contracts which must survive that persistence.
A fallback must preserve facts, not imitate a row
The retained export path had a reasonable recovery rule: when the local user row was absent, construct the minimum account payload from the identifier already supplied to the action.
The initial fallback assigned that value to a generic local field. Downstream serialization read a different field for the export identity:
input:
stable external account id
fallback:
local id = external account id
export id = null
queued contract:
export id must be a stringGiving a temporary object something which looks like a database primary key did not make it a persisted row. The useful fact was simpler: the export still described the external account named by the input.
The boundary model should preserve that fact explicitly:
fallback external account id = supplied external account id
fallback local row id = absent
fallback local public id = absentThe production object retained a synthetic local value for compatibility, but the important correction was assigning the external identifier to the field the export contract actually used. The absent local public identifier remained null instead of being fabricated.
That distinction keeps the fallback honest. It knows the subject. It does not pretend the newer database contains a row which is not there.
Stable does not mean meaningful to humans
An email address, company number, username, or account name may look like a convenient shared identifier. Each carries change rules which may be outside the application’s control.
Email addresses change. Names collide. Regulatory identifiers can be corrected. Accounts can merge or split. Reusing any of them as identity couples a business operation to a mutable attribute.
A durable identifier should be:
- unique in its declared namespace;
- immutable for the lifetime of the entity;
- opaque enough that changing an attribute does not change identity;
- preserved through imports and storage migrations;
- explicit about its issuing system or namespace; and
- never reassigned to a different entity.
Opacity alone is not enough. A random value which is regenerated during every import is opaque but not stable. A stable identifier is a lifecycle agreement, not a formatting choice.
Imports need mappings, not identity replacement
Moving an account from an older store into a new one usually creates a new local row:
older external identity: acct_demo_7f3
new local row: 4821
new public id: usr_demo_c92The import should preserve the external identity or record a mapping to it. It
should not quietly promote 4821 as the new identity for every integration.
Mappings also make collisions and merges visible. If two source records claim the same durable identifier, the importer should stop for resolution. If two identities are intentionally merged, retain aliases or a redirect record according to the domain’s audit and retention requirements.
Destroying the old identifier after a merge turns late messages and historic records into mysteries. Preserving it as an alias lets the system recognize the old reference without pretending two active accounts still exist.
Queues turn identity mistakes into delayed failures
Synchronous code can load a row and use it immediately. Queued work separates capture from execution:
dispatch -> wait -> retry -> executeAny state consulted only during execution may have changed. The row may be deleted, reimported, or temporarily unavailable. The export may also be replayed after a deployment or recovery.
Capture the durable subject identity when the work is created. Capture immutable facts needed for the effect when the contract permits it. At execution, resolve current mutable attributes deliberately rather than relying on an accidental mix of old and new state.
An idempotency key still has a separate job. It can prevent two attempts from creating the same invoice or remote account, but it does not say which account the effect belongs to unless that subject identity is part of the key’s declared scope.
Test the missing representation, not only the normal row
The normal case hides this class of defect:
local row exists
external identifier is populated on the row
export succeedsThe useful regression begins with the boundary condition:
external account exists
local row is absent
fallback is constructed
queued export receives the original external identity
local-only identity remains absentThe DATA-07 fixture models that contract with synthetic identifiers. It shows the broken fallback producing a null export identity, then verifies that the corrected payload names the same account before and after a local storage replacement. Replaying the payload preserves the subject; the fixture does not claim to prove remote idempotency.
The retained evidence establishes the field mismatch and its correction. It does not establish how often the path failed, how many accounts were involved, or what users observed.
Identity should follow the entity, not its current row
Ask of every identifier:
Who issues it?
What namespace makes it unique?
Can it change?
Can it be reused?
Which boundary understands it?
How long must messages containing it remain valid?Keep the database primary key when the database owns the relationship. Keep a public identifier when exposure and enumeration are concerns. Keep a durable external identifier when another system, an import, or a delayed message must recognize the same entity.
Storage will change. Rows will be missing. Jobs will run late. The identifier at a boundary has done its job only if the subject remains the same through all three.