Entities
Entities are domain objects with identity that persists through state changes — distinguished by ID, not attribute values.
Introduction
Entities are domain objects with identity that persists through state changes — distinguished by ID, not attribute values. LinkedIn's Member, Connection, and JobPosting entities have lifecycles spanning years; architecture must track identity, authorization, and evolution without equating entities with database rows blindly.
Real production story
LinkedIn messaging refactored "Conversation" as a value object keyed by participant hash — when a member changed primary email, conversations forked and messages appeared lost. Root cause: identity was derived from mutable attributes instead of stable ConversationId assigned at creation. Re-modeling Conversation as entity with immutable ID, explicit merge rules for account linking, and event log of identity changes restored message threads. Member support tickets for "missing messages" dropped 70%; data migration backfilled IDs from audit log.
Business problem
LinkedIn graph entities live for decades with merges, splits, and privacy actions. Treating entities as dumb rows or value objects destroys identity continuity — member trust and regulatory erasure requests both fail.
- Identity continuity: Account merge must combine graph entities without duplicating connections.
- Compliance: GDPR erasure targets entity ID — must trace all child entities.
- Evolution: Entity lifecycle states (active, dormant, restricted) drive different business rules.
Architecture overview
Entity equality by identity (ID), not attribute equality. Entities encapsulate lifecycle transitions with domain rules — member.restrict() not member.status = 'restricted' from controller.
- Identity: Surrogate UUID/ULID assigned at creation — stable forever.
- Lifecycle: State machine methods enforce valid transitions.
- Inside aggregate: Some entities are internal to root — not referenced externally.
- Persistence: ORM entity ≠ domain entity — map in repository layer.
Architecture motivation
Entities carry identity and lifecycle inside aggregates. Staff design distinguishes entity (has ID, mutable state) from value object (no ID, replaced wholesale) — confusion causes subtle production bugs.
- Force: Long-lived professional graph with account linking and impersonation rules.
- Constraint: Legacy schemas use natural keys — migrate to surrogate IDs with mapping table.
- Outcome: Entity lifecycle documented; ID never derived from mutable fields.
Internal architecture
LinkedIn messaging entity model inside Conversation aggregate:
- Message is entity inside Conversation — referenced by MessageId within thread only.
- Member merge emits MemberMerged event — Conversation ACL updates participant refs.
Conversation (aggregate root)├── ConversationId (entity identity — immutable)├── ParticipantRefs (MemberId value refs)├── Messages[] (Message entities — internal)│ └── MessageId, body, sentAt, editHistory└── lifecycle: Active · Archived · MemberErasedMember (separate aggregate root)├── MemberId├── profile attributes (value objects)└── lifecycle: Active · Merged · Restricted · Deleted
Data flow
Entity creation: assign ID at birth → persist → emit Created event. Lifecycle: domain method validates transition → update → emit StateChanged event. Merge: identity mapping table records old→new IDs for async consumers.
- Create message: Conversation.addMessage() creates Message entity with new MessageId.
- Account merge: Member.mergeInto(target) reassigns foreign refs via event consumers.
- Erasure: MemberDeleted triggers Conversation redaction — entity tombstone, not hard delete row scan.
System design diagram
Two diagrams show the Entities topology and the primary request/event path used in production at scale.
Production code example
Member entity with lifecycle — Java domain entity LinkedIn-style:
- ID field final — entity identity immutable even when profile value objects change.
- Lifecycle methods return domain events for downstream identity mapping consumers.
public class Member {private final MemberId id;private MemberStatus status;private Profile profile;public Member(MemberId id, Profile profile) {this.id = Objects.requireNonNull(id);this.profile = profile;this.status = MemberStatus.ACTIVE;}public MemberMerged mergeInto(Member target) {if (this.status == MemberStatus.DELETED) {throw new IllegalStateException("Cannot merge deleted member");}if (!this.id.equals(target.id)) {this.status = MemberStatus.MERGED;return new MemberMerged(this.id, target.id, Instant.now());}throw new IllegalArgumentException("Cannot merge member into self");}public MemberRestricted restrict(RestrictionReason reason, Actor admin) {admin.requireRole(Role.TRUST_SAFETY);this.status = MemberStatus.RESTRICTED;return new MemberRestricted(this.id, reason, admin.id());}public MemberId id() { return id; } // identity never changes}
Enterprise case study
LinkedIn Conversation identity fix: Value-object identity caused forked threads on email change.
- Before: Participant-hash key; 12k monthly "missing message" tickets.
- Decision: Conversation entity with UUID; MemberMerged event; identity mapping service.
- After: Tickets down 70%; merge completes in <5 min async; GDPR erasure traceable by ID.
Trade-offs
- Surrogate vs natural key: Natural keys (email) change — surrogate ID with lookup table.
- Rich vs anemic entity: Rich domain methods vs service layer procedural — prefer rich for core entities.
- Entity vs value object: Address as VO if no tracking needed; as entity if verification history matters.
- Soft delete vs event erasure: Compliance may require crypto-shred — entity tombstone pattern.
Security considerations
Entity ID is authorization subject: Every mutation checks actor against entity ownership graph.
- IDOR: Never expose sequential IDs — use opaque IDs and authz on every fetch.
- Impersonation: Admin actions on Member entity require break-glass audit event.
- Erasure: Entity deletion propagates as signed domain events — consumers must comply.
Scalability analysis
Billions of entities require shard-by-entity-ID, lazy lifecycle archival, and identity mapping service for merges at scale.
- Shard key: MemberId hashes to partition — related entities co-locate when possible.
- Archival: Dormant Conversation entities tier to cold storage — identity preserved.
- Merge throughput: Account linking batch updates via event replay — not synchronous fan-out.
Failure scenarios
Entity identity failures: Duplicate IDs, forked identity on attribute change, merge without idempotency.
- Forked thread: Natural key hash changed — assign immutable ConversationId at create.
- Double merge: Retry merges same members — idempotent merge command on MemberId pair.
- Orphan messages: Delete member without cascade plan — event-driven redaction pipeline.
Staff engineer insights
- If you compare entities with equals() on fields, you probably wanted a value object — entity equality is ID only.
- Account merge is the stress test for entity design — if you haven't modeled merge, identity is incomplete.
- ORM annotations on domain entities leak persistence — map in repository to keep lifecycle methods testable.
Interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1AdvancedQuestionEntity vs value object — decision criteria?+
Answer
Follow-up
2AdvancedQuestionDesign entity model for account merge in social graph.+
Answer
Follow-up
3AdvancedQuestionShould domain entities have JPA annotations?+
Answer
Follow-up
Architecture review questions
- Do entities have immutable surrogate identity assigned at creation?
- Are lifecycle transitions enforced by domain methods with events?
- Is entity equality based on ID only?
- Are internal entities hidden — only root referenced externally?
- Is account merge/linking modeled with identity mapping?
- Is domain entity free of ORM/framework annotations?
Summary
Entities at LinkedIn model long-lived identity for members and conversations — stable IDs, rich lifecycle methods, and merge events — fixing forked threads and enabling compliant erasure across the professional graph.