Decoupling Archetypes: Identity Over Inheritance

Inheritance looks elegant until it couples your entire architecture. Learn why Business Archetypes should be connected by identity, not shared base classes, and how this keeps Bounded Contexts independent and maintainable.

Decoupling Archetypes: Identity Over Inheritance

If you would like to see archetypes applied in practice, I maintain an open-source repository demonstrating archetypes implemented alongside Domain-Driven Design patterns: https://github.com/smalaca/archetypes


In the previous article, we explored the structural anatomy of the Party archetype. We saw how it separates core identity from specialized types like Person and Organization, and how Roles and Relationships define behavior. Now, we’ll look at the technical implementation strategy that keeps these models clean and maintainable.

When engineers first discover Business Archetypes, their instinct is often to reach for the most powerful tool in the Object-Oriented toolbox: Inheritance.

It seems logical. If a Person is a Party and an Organization is also a Party, why not create a Party base class and have everything else inherit from it? It feels clean, DRY, and "correct" from a modeling perspective.

But in a modern, modular architecture—especially one guided by Domain-Driven Design—this is a trap. Direct inheritance across Bounded Contexts is a shortcut to a distributed monolith.

The Inheritance Trap: The Wrong Way #1

The problem with inheritance is that it creates the strongest possible coupling between concepts. Consider this "clean" shared library approach using the Party Archetype:

// Shared Library or Core Module - WRONG
package com.smalaca.shared.core;

@ArchetypeParty.Party
public abstract class Party {
    private final PartyId id;
    private final List<Address> addresses;
    private final TaxNumber taxNumber; // Needed by Accounting
    private final List<Role> roles;     // Needed by IAM
    
    // Logic that operates over fields not used by everyone
    public void validateTaxInformation() { ... } 
}

// Users Context
package com.smalaca.users;

@ArchetypeParty.Person
public class User extends Party { ... }

// Accounting Context
package com.smalaca.accounting;

@ArchetypeParty.PartyRole
public class Buyer extends Party { ... }

This approach leads to several critical issues:

  1. Polluted Abstractions: You end up putting fields (like taxNumber) in the Party base class that are only used by one or two contexts, forcing every other context to carry that "dead weight."
  2. Context-Specific Logic Leakage: Methods like validateTaxInformation() start appearing in the base class because they operate on Party fields, even though they only make sense in the Accounting context. This makes the "base" model increasingly fragile.
  3. Synchronization Nightmares: In a distributed system, if Party is a shared entity, synchronizing changes across microservices becomes a bottleneck. Every time a field in the base class changes, you risk breaking binary compatibility or requiring a coordinated deployment of all services.

The "God-Object" Trap: The Wrong Way #2

Another common mistake is trying to keep everything in a single, "omnipotent" entity while attempting to apply the Party Archetype. We see this most often with a bloated User class that tries to play all roles at once.

// One Entity for everything - WRONG
@ArchetypeParty.Person
public class User {
    @ArchetypeParty.PartyIdentifier
    private final UserId id;
    private final String login;
    
    @ArchetypeParty.PartyRole
    private final List<Role> roles;
    
    // Fields for the 'Student' role
    private final List<CourseId> enrolledCourses;
    
    // Fields for the 'Trainer' role
    private final String bio;
    
    // Fields for the 'Buyer' role (Accounting)
    private final TaxNumber taxNumber;
    
    // ... 50 more nullable fields
}

This is dangerous for several reasons:

  1. Data Leakage: You have more details than you should in any given context. A developer working on Course Enrollment might accidentally use taxNumber just because it's available on the User object, creating a dependency that shouldn't exist.
  2. Cognitive Load: It becomes incredibly hard to determine which data is actually useful or "active" in a specific business flow.
  3. Constraint Violation: Because the logic for different roles is mashed together, you may violate business constraints by modifying a field in the wrong place. For example, updating a bio might accidentally trigger logic that was intended for taxNumber validation, leading to corrupted state or unexpected side effects.

The Solution: Reference by Identity

Instead of forcing a shared class hierarchy or a single bloated entity, we should treat each Bounded Context as an independent world. We still use the Party Archetype, but we implement it differently: each context has its own Party representation, and they are linked only by a simple Identity.

In the Training Center project, we don't share a Party class. Instead, we use specific IDs and context-specific roles to bridge the gap while maintaining the Party Archetype structure.

Consider how the Party is represented in different contexts:

package com.smalaca.trainingcenter.users;

// Users Management Context
@ArchetypeParty.Person
public class User {
    @ArchetypeParty.PartyIdentifier
    private final UserId userId;
    private final String login;
    private final PersonalData personalData;
}

package com.smalaca.trainingcenter.accounting;

// Accounting Context
@ArchetypeParty.PartyRole
public class Buyer {
    @ArchetypeParty.PartyRoleIdentifier
    private final BuyerId buyerId; // Same value as UserId
    private final TaxNumber taxNumber;
    private final List<Invoice> invoices;
}

Notice the separate classes. This solves the problems mentioned in the traps:

  1. Solves the Inheritance Trap: The Buyer does not inherit from User or a shared Party base class. If User needs to add a loginHistory, the Buyer class in the Accounting context is completely unaffected. There is no shared base class to synchronize.
  2. Solves the God-Object Trap: The Buyer class contains only the data it needs for accounting. It doesn't have a login or passwordHash. The User class doesn't have a taxNumber. The boundaries are enforced by the type system and the physical separation of classes.
  3. Semantic Clarity: A BuyerId in the Accounting context tells you exactly what this ID represents there, even if it refers to the same human being as UserId.

The Trade-offs

Choosing identity over inheritance is not a "free lunch." You must be aware of the costs:

  1. Multiple Perspectives: The same physical entity (e.g., a person) will be represented through various perspectives across the system. This is intentional, but it requires discipline.
  2. Distributed Consistency: You must carefully control where the "source of truth" for specific fields lies. For example, if a name changes, you need a strategy (like Domain Events) to propagate that change if other contexts need the updated value.
  3. Data Duplication: In distributed systems, you might end up duplicating some data (like a name or email) to keep services autonomous. The cost of this duplication and the complexity of eventual consistency is the price you pay for architectural flexibility and independent scalability.

The Rule of Engagement

When implementing archetypes across your system, follow this rule: Align on structure, decouple on implementation.

  1. Shared Identity: Use UUIDs or strongly-typed IDs to refer to the same logical entity across Bounded Contexts.
  2. Context-Specific Data: Only include the data that is relevant to the current context. Don't pull in "nice-to-have" fields from other domains.
  3. Archetype Blueprints: Use archetypes to ensure that even though the implementations are separate, they follow the same business logic "shapes" (e.g., how a Party interacts with Roles).

Summary

Decoupling is not about avoiding commonality; it's about avoiding the wrong kind of commonality. Business Archetypes give you a shared language and structure, while Identity-based references give you the freedom to evolve.

Don't let the allure of inheritance or the "one size fits all" entity lead you into a maintenance nightmare. Keep your archetypes clean, keep your contexts bounded, and use IDs to tell the story of how they connect.