Software Archetypes

The Party Pattern

Based on "Enterprise Patterns and MDA" by Jim Arlow & Ila Neustadt

Problem

You need to model people and organizations in your system, but:

  • People and organizations share common behaviors (name, contact info, addresses)
  • They also have unique attributes (person has birthdate, organization has registration number)
  • You want to avoid duplicating code for "Customer who is a Person" vs "Customer who is a Company"
  • Queries like "find all parties in Warsaw" should work regardless of party type

Concept

Party is an abstraction representing any entity that can enter into relationships, own things, or participate in transactions. A Party is either a Person (individual) or Organization (company, institution, group).

This allows you to write code against "Party" and have it work for both people and organizations.

Structure

1

0..*

«abstract»

Party

+id: UUID

+name: String

+addresses: Address[]

+contacts: ContactMethod[]

+createdAt: Timestamp

+getName() : : String

+getPrimaryAddress() : : Address

Person

+firstName: String

+lastName: String

+birthDate: Date

+gender: Gender

+getName() : : String

Organization

+legalName: String

+tradingName: String

+registrationNumber: String

+taxId: String

+getName() : : String

Address

+street: String

+city: String

+postalCode: String

+country: String

+type: AddressType

+isPrimary: Boolean

Attributes

Party (Abstract)

Attribute Type Required Description
id UUID Yes Unique identifier
addresses Address[] No Collection of addresses
contacts Contact[] No Phone, email, etc.
createdAt Timestamp Yes When record was created
updatedAt Timestamp Yes Last modification time

Person

Attribute Type Required Description
firstName String Yes Given name
lastName String Yes Family name
middleName String No Middle name(s)
birthDate Date No Date of birth
gender Enum No Male, Female, Other, Unknown

Organization

Attribute Type Required Description
legalName String Yes Official registered name
tradingName String No "Doing business as" name
registrationNumber String No Company registration ID
taxId String No Tax identification number
foundedDate Date No When organization was founded

Address

Attribute Type Required Description
street String Yes Street address with number
city String Yes City name
postalCode String Yes Postal/ZIP code
country String Yes ISO country code (PL, US, etc.)
type AddressType Yes Home, Work, Billing, Shipping
isPrimary Boolean Yes Is this the default address?

Behaviors

Entity Party (abstract):
    
    function getName(): String
        // Abstract - implemented by subclasses
    
    function getPrimaryAddress(): Address or None
        return addresses.find(a => a.isPrimary == true)
    
    function addAddress(address: Address):
        if address.isPrimary:
            // Ensure only one primary per type
            for existing in addresses:
                if existing.type == address.type:
                    existing.isPrimary = false
        addresses.add(address)
    
    function getAddressesByType(type: AddressType): Address[]
        return addresses.filter(a => a.type == type)


Entity Person extends Party:
    
    function getName(): String
        if middleName is not empty:
            return firstName + " " + middleName + " " + lastName
        return firstName + " " + lastName
    
    function getAge(): Integer or None
        if birthDate is None:
            return None
        return yearsBetween(birthDate, today())
    
    function isAdult(): Boolean
        age = getAge()
        return age is not None and age >= 18


Entity Organization extends Party:
    
    function getName(): String
        if tradingName is not empty:
            return tradingName
        return legalName
    
    function getOfficialName(): String
        return legalName

Examples

Person: Jan Kowalski

jan = new Person(
    firstName: "Jan",
    lastName: "Kowalski",
    middleName: "Adam",
    birthDate: 1985-03-15,
    gender: Gender.Male
)
jan.addAddress(new Address(
    street: "ul. Marszalkowska 10/5",
    city: "Warszawa",
    postalCode: "00-001",
    country: "PL",
    type: AddressType.Home,
    isPrimary: true
))

jan.getName()   // "Jan Adam Kowalski"
jan.getAge()    // 41
jan.isAdult()   // true

Organization: Acme Sp. z o.o.

acme = new Organization(
    legalName: "Acme Sp. z o.o.",
    tradingName: "Acme",
    registrationNumber: "0000123456",
    taxId: "5261234567"
)
acme.addAddress(new Address(
    street: "ul. Polna 22",
    city: "Krakow",
    postalCode: "30-001",
    country: "PL",
    type: AddressType.Work,
    isPrimary: true
))

acme.getName()          // "Acme" (tradingName)
acme.getOfficialName()  // "Acme Sp. z o.o."

Invariants

  1. Party must have a name — Person requires firstName + lastName; Organization requires legalName
  2. Only one primary address per type — Cannot have two primary billing addresses
  3. Person age cannot be negative — birthDate must be in the past
  4. Organization registration is unique — No two organizations share the same registrationNumber (within a jurisdiction)

Relationships

From To Cardinality Description
Party Address 1 to many Party has multiple addresses
Party ContactMethod 1 to many Party has multiple contacts
Party PartyRole 1 to many Party plays multiple roles
Organization Person many to many Via employment relationship

Error Handling

Operation Precondition Error
new Person(...) firstName and lastName required ValidationError: Person requires firstName and lastName
new Organization(...) legalName required ValidationError: Organization requires legalName
addAddress(isPrimary=true) — No error; existing primary of same type is demoted
Person.getAge() birthDate is set Returns None if birthDate is null (no error)

SQL Schema

sql-- Using single-table inheritance with discriminator column
CREATE TABLE party (
    id              UUID PRIMARY KEY,
    party_type      VARCHAR(20) NOT NULL,  -- 'Person' or 'Organization'
    
    -- Person fields
    first_name      VARCHAR(100),
    last_name       VARCHAR(100),
    middle_name     VARCHAR(100),
    birth_date      DATE,
    gender          VARCHAR(10),
    
    -- Organization fields
    legal_name      VARCHAR(255),
    trading_name    VARCHAR(255),
    registration_number VARCHAR(50),
    tax_id          VARCHAR(50),
    founded_date    DATE,
    
    -- Common fields
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    -- Constraints
    CONSTRAINT chk_party_type CHECK (party_type IN ('Person', 'Organization')),
    CONSTRAINT chk_person_name CHECK (
        party_type != 'Person' OR (first_name IS NOT NULL AND last_name IS NOT NULL)
    ),
    CONSTRAINT chk_org_name CHECK (
        party_type != 'Organization' OR legal_name IS NOT NULL
    )
);

CREATE TABLE address (
    id              UUID PRIMARY KEY,
    party_id        UUID NOT NULL REFERENCES party(id) ON DELETE CASCADE,
    street          VARCHAR(255) NOT NULL,
    city            VARCHAR(100) NOT NULL,
    postal_code     VARCHAR(20) NOT NULL,
    country         CHAR(2) NOT NULL,  -- ISO 3166-1 alpha-2
    address_type    VARCHAR(20) NOT NULL,  -- 'Home', 'Work', 'Billing', 'Shipping'
    is_primary      BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Index for common queries
CREATE INDEX idx_party_type ON party(party_type);
CREATE INDEX idx_address_party ON address(party_id);
CREATE INDEX idx_address_type ON address(party_id, address_type);

Design Considerations

Single Table vs Joined Table Inheritance

Approach Pros Cons
Single Table Simple queries, fast Many NULL columns, wasted space
Joined Table Normalized, type-specific indexes Complex queries, more JOINs
Concrete Table No NULLs, isolated types Cannot query all parties easily

Recommendation: Single table for Party (few type-specific columns), Joined for complex hierarchies.

When to Use Party Pattern

✅ Use when:

  • Both people and organizations can be customers/suppliers/employees
  • You need unified queries across party types
  • Shared behaviors (addresses, contacts, roles)

❌ Don't use when:

  • Only dealing with one type (just people OR just organizations)
  • Types have vastly different attributes with no overlap
  • Simple CRUD with no business logic