Party Archetype - Time for architecture that doesn't hurt

Party Archetype - Time for architecture that doesn't hurt

We've all seen it. You open a project on Friday afternoon, look at the main model, and see the User class. It has 120 columns, 3,000 lines of code, and methods responsible for everything from logging in to issuing invoices to handling newsletters. Then the business comes up with a "simple change": we want to serve B2B partners who can also log in, but have a VAT number instead of a personal identification number, and sometimes are also regular customers. What do you do? You add an is_partner flag, another set of nullable fields, and pray that nothing explodes in production.

This is a classic anti-pattern that leads to monolithic, "god objects", data duplication, and code that is a nightmare to maintain. The problem is that we are trying to model the real world through rigid classes representing roles, instead of focusing on who plays those roles. However, there is a better approach, known in the world of Domain-Driven Design and advanced enterprise architecture. Let's talk about the Party archetype.

The problem with role inheritance

Let's imagine a system for a large university. At the beginning, we have the Student class. Simple. Then comes Professor. Also simple, they have different permissions and data. The trouble starts when it turns out that a professor can enrol in a course as a hobby (becoming a student), and a doctoral student is both a student and a lecturer.

The traditional approach leads to one of two bad solutions. The first is to create separate tables and classes for each role, which ends up duplicating John Smith's personal data in three different places in the system. The second is to attempt inheritance or use modules (mixins in Ruby), which quickly ends up in spaghetti code, where PhdStudent inherits from Student and includes EmployeeBehaviour, and we drown in conditions checking if user.is_a? (Professor). This is a road to nowhere, because we confuse who someone is with what someone does in a given context.

Party architecture, or party archetype

The solution is to use the "Party Archetype" pattern. The name can be misleading, because it is not about partying, but about a “party” in the legal or business sense (like a "party to a contract").

The key concept is the fundamental separation of entity from role. An entity is who you are, regardless of circumstances - a natural person or an organisation. A role is a hat you wear in a given business context - student, customer, supplier or employee.

In this model, we create an abstract base class Party, which represents any entity in the system. It has a unique global identifier and possibly common mechanisms, such as address or contact handling. Two main, concrete entities inherit from this class: Person (natural person) and Organisation (company, department, university).

Let's see how this might look in Ruby in a simplified form.

# Base abstract class for any legal entity within the system.
# It holds the global identity and common behavior.
class Party
  attr_reader :id, :global_id

  def initialize(id:, global_id:)
    @id = id
    @global_id = global_id
    @roles = [] # Collection of dynamic roles
  end

  # Methods to manage roles dynamically
  def add_role(role)
    @roles << role
  end

  def has_role?(role_name)
    @roles.any? { |r| r.name == role_name }
  end
end

# Represents a human being.
# Contains personal data unrelated to any specific business context.
class Person < Party
  attr_accessor :first_name, :last_name, :date_of_birth

  def initialize(id:, global_id:, first_name:, last_name:)
    super(id: id, global_id: global_id)
    @first_name = first_name
    @last_name = last_name
  end
end

# Represents an organization, company, department, etc.
class Organization < Party
  attr_accessor :legal_name, :tax_id

  def initialize(id:, global_id:, legal_name:)
    super(id: id, global_id: global_id)
    @legal_name = legal_name
  end
end

Thanks to this approach, John Smith is only in the system once as an instance of the Person class. His first name, last name, and personal identification number are stored in one place. There is no risk of data desynchronisation.

Roles as context, not identity

Now the most important thing: roles are not classes that we inherit from. Roles are separate entities that are assigned to Party. A role defines behaviour and data specific to a given context. The "Student" role will contain a student ID number and grade history, while the "Employee" role will contain an hourly rate and bank account number.

In advanced systems, roles are often modelled as separate aggregates that "point" to Party ID, rather than keeping the collection of roles directly in the Party object. This provides better scalability and allows only the data that is needed for a given business process to be loaded.

# A base class for roles, representing a context specific hat a Party wears.
class Role
  attr_reader :party_id, :name, :active_since

  def initialize(party_id:, name:)
    @party_id = party_id
    @name = name
    @active_since = Time.now
  end
end

# Specific role implementation with context-specific data
class StudentRole < Role
  attr_accessor :student_index_number

  def initialize(party_id:, index_number:)
    super(party_id: party_id, name: :student)
    @student_index_number = index_number
  end
  # Domain logic specific to being a student goes here
end

# Usage example:
# We create Jan once.
jan = Person.new(id: 1, global_id: "UUID-123", first_name: "Jan", last_name: "Kowalski")

# Jan becomes a student. We associate the role, not mutate Jan's class.
student_context = StudentRole.new(party_id: jan.id, index_number: "S12345")

# Later, Jan gets hired by the university.
employee_context = Role.new(party_id: jan.id, name: :employee)

# We can easily check what Jan "is" in current context without flags
# In a real DB scenario, this would be a query on roles table by party_id.
puts "Is Jan a student? #{student_context.party_id == jan.id}"

Relationships are also separate entities

The final piece of the puzzle is the relationships between entities. In the old model, if a Student belonged to a Department, we added a foreign key department_id to the students table. In the Party model, relationships are often too complex for simple foreign keys. Relationships can be asymmetrical, have time frames and their own attributes (e.g. the "Employment" relationship between a Person and an Organisation has a start date, end date and contract type). Therefore, relationships should also be modelled as separate entities (e.g. a Relationship class connecting two Parties and defining the type of their relationship).

Why switch to this model?

Using the Party archetype requires a certain mental shift and may seem redundant at first. However, as the complexity of the system increases, this investment pays off many times over. You gain flexibility because adding a new business role (e.g., "Affiliate Partner") does not require changing the structure of the person database, but only adding a new role type. You avoid data duplication, which is crucial for consistency and compliance with regulations such as GDPR. Most importantly, your code begins to reflect business reality rather than the technical limitations of your ORM framework. This approach distinguishes mature architecture from quick fixes.

Happy PARTY-ing!