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 and its stable identifier reached the export action. What did not exist was a matching row in the newer application’s user table.
The action worked around that absence by building a temporary object with safe fallback details. It even gave the object a synthetic local identifier. The field used as the export identity remained null, though, and a queued data object later rejected the payload because that field had to be a string. The fallback had solved the missing attributes while losing the one fact the export actually needed: which account it was about.
One entity can have several legitimate identifiers
Applications tend to call a field id long after that name has stopped being
useful. That is harmless while there is only one identifier in play. Once an
account crosses systems, the generic name starts hiding different jobs and
different lifetimes:
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 effectA 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. Shoving all of them behind id does not make them interchangeable; it
only makes the eventual mistake harder to spot.
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 just cannot be the only way to determine the subject when the receiver does not share the same table. This is not an argument for one global identifier everywhere. Local keys belong to local persistence; durable identifiers belong in contracts which must survive it.
A fallback must preserve facts, not imitate a row
The 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 does not magically turn it into a persisted row. The useful fact was much 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 without pretending 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 regenerated during every import is still a bad identifier for anything expected to survive the import. Stability 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. The right mapping depends on the domain, but silently replacing identity is not a mapping at all.
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. The queue did not create the identity mistake; it merely waited long enough to expose it.
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 is almost useless for finding 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 repository history 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. A green normal-case test proves that the row exists in the normal case; it says nothing about whether the fallback preserves identity.
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. The important part is not picking the most impressive identifier format. It is being honest about which lifetime the identifier has to survive.
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.