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
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 legalNameExamples
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() // trueOrganization: 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
- Party must have a name — Person requires firstName + lastName; Organization requires legalName
- Only one primary address per type — Cannot have two primary billing addresses
- Person age cannot be negative — birthDate must be in the past
- 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
Related Patterns
- Party Role — What role does a Party play? (Customer, Supplier, Employee)
- Party Relationship — How are Parties related? (Employment, Ownership)
- Party Identifier — External IDs (Passport, Tax ID, Customer Number)