ArchitectMind
  1. Home
  2. Tutorials
  3. Book 5 · Software Design & Design Patterns
  4. Volume II
  5. Relationships
Book 5 · Volume II — Object-Oriented Design & SOLID · Topic 1.3

Relationships

Association, aggregation, composition and dependency

9 min readStage 2 · Frameworks & design8 interview questions
Chapter
Chapter 1 — Object-Oriented Design
Prerequisites
Topics 1.1 and 1.2; Volume I Topic 3.4 (relationships and direction)
You will be able to
Distinguish the four class relationships in UML and Java code, choose the weakest relationship that fits, model ownership and lifecycle correctly, and map relationships to persistence and service boundaries
Covers
Association · Aggregation · Composition · Dependency
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — Relationships in Code and Memory
  5. 4A. Deep Dive — Relationships and Aggregates
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

A relationship describes how classes are connected. The four relationships in the TOC differ in strength (how tightly they couple the classes), duration (temporary use vs long-lived reference) and ownership (whether one object controls another's lifecycle).

Relationships in UML and Java
Figure 1 — Relationships in UML and Java
RelationshipMeaningDurationOwnershipUML
DependencyA uses B temporarilyOne operationNoneDashed arrow
AssociationA knows B (holds a reference)Long-livedNoneSolid line/arrow
AggregationA has B as a part; parts can exist independently/sharedLong-livedWeak (no lifecycle control)Hollow diamond
CompositionA consists of B; parts live and die with the wholeLong-livedStrong (lifecycle control)Filled diamond
Table 1 — The four relationships

2Why It Exists

Relationship semantics come from OO modelling methods (Booch, Rumbaugh's OMT, Jacobson) merged into UML (1997). Aggregation and composition distinguish "whole–part" structures common in domains (orders and lines, documents and paragraphs). Domain-Driven Design refined composition into aggregates — consistency boundaries with a root that owns its parts — which strongly influence persistence and microservice data design.

3Core Concepts

3.1 Association

An association is a structural relationship: an object holds a reference to another for longer than a single operation. Associations have direction (who knows whom) and multiplicity (one, many, optional).

JAVA
// Unidirectional association: Order knows its customer
// (by ID — preferred across aggregates)
public final class Order {
    private final CustomerId customerId;    // reference by identity
    …
}
 
// Object reference association within a module
public final class ReminderService {
    private final Mailer mailer;            // long-lived collaborator
    public ReminderService(Mailer mailer) { this.mailer = mailer; }
}
Design choiceGuideline
DirectionPrefer unidirectional; bidirectional associations double the coupling and must be kept consistent
MultiplicityMake it explicit (Optional, List, required constructor parameter)
Reference vs IDAcross aggregates/modules/services, hold IDs, not object references
Navigation depthAvoid deep navigation chains (Law of Demeter)
Table 2 — Association design

3.2 Aggregation

Aggregation is a whole–part association where parts can exist independently of the whole and may be shared among several wholes.

JAVA
// A Team aggregates Players; players exist without the team and can join other teams
public final class Team {
    private final TeamId id;
    private final Set<PlayerId> members = new HashSet<>();   // references by identity
    public void add(PlayerId player) { members.add(player); }
    public void remove(PlayerId player) { members.remove(player); }
}

UML's aggregation semantics are notoriously weak — the UML specification itself leaves them loosely defined — so many teams simply model aggregation as an association with "has-a" meaning. What matters in practice: the whole does not control the parts' lifecycle.

3.3 Composition

Composition is a strong whole–part relationship: the whole owns its parts, creates them, controls access to them, and deletes them when it is deleted. A part belongs to exactly one whole at a time.

Aggregation vs composition: who owns the lifecycle?
Figure 2 — Aggregation vs composition: who owns the lifecycle?
JAVA
public final class Order {
    private final List<OrderLine> lines = new ArrayList<>();   // composed parts
 
    public void addLine(Sku sku, int qty, Money price) {       // whole creates parts
        lines.add(new OrderLine(sku, qty, price));
    }
    // no external mutation
    public List<OrderLine> lines() { return List.copyOf(lines); }
}
JAVA
// JPA mapping of composition: cascade and orphan removal express lifecycle ownership
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderLineEntity> lines = new ArrayList<>();
Composition ruleReason
Parts created by the whole (Creator)Invariants checked on creation
Parts not exposed for modificationWhole enforces consistency
Parts deleted with the wholeNo orphans
Access to parts only through the wholeAggregate root (DDD)
Table 3 — Composition rules

3.4 Dependency

A dependency is the weakest relationship: A uses B during an operation — as a parameter, local variable, return type or static call — without keeping a reference.

JAVA
// Dependency: Clock used only for this call
public Invoice issue(Order order, Clock clock) {
    return new Invoice(order.id(), order.total(), LocalDate.now(clock).plusDays(30));
}
Relationship strength
Figure 3 — Relationship strength
Dependency formCoupling
Parameter of an interface typeWeak — caller supplies implementation
Parameter of a concrete classModerate
new ConcreteClass() inside a methodStronger — hard-wired
Static method call on another classStronger — hard to substitute
Checked exception type in signatureCallers depend on it too
Table 4 — Forms of dependency

4How It Works Internally — Relationships in Code and Memory

1. Dependency exists only in bytecode (constant pool references, method signatures); at runtime no reference is kept after the call.

2. Association/aggregation/composition are fields holding references (or IDs); the GC keeps referenced objects alive while reachable.

3. Composition in memory: parts are reachable only through the whole, so when the whole becomes unreachable, parts are collected too.

4. In persistence, composition maps to cascading operations and orphan removal; associations across aggregates map to foreign keys/IDs without cascades.

5. At module level, every relationship is also a compile-time dependency between packages; the direction rules of Volume I Topic 3.4 apply.

4ADeep Dive — Relationships and Aggregates

4A.1 From composition to DDD aggregates

ConceptMeaning
AggregateA cluster of objects treated as one unit for consistency
Aggregate rootThe only entity outside objects may reference; enforces invariants
Inside the aggregateComposition (lines of an order)
Between aggregatesAssociation by ID only (order → customer ID)
Transaction boundaryOne aggregate per transaction (guideline)
Table 5 — Aggregate rules (Evans, Vernon)

4A.2 Why reference other aggregates by ID

Object reference across aggregatesID reference
Loads large object graphs (lazy loading, N+1)Load only what you need
Tempts modification of other aggregates in one transactionClear consistency boundary
Couples persistence mappingsIndependent persistence
Impossible across servicesWorks across services
Table 6 — ID references scale
Choosing a relationship in Java
Figure 4 — Choosing a relationship in Java

5APIs / Features / Syntax

RelationshipJava/JPA expression
DependencyMethod parameter/local/return type; static call
AssociationField (private final Mailer mailer) or ID field (CustomerId)
AggregationCollection of IDs or shared references; @ManyToMany or join table
CompositionPrivate collection created internally; @OneToMany(cascade = ALL, orphanRemoval = true), @Embedded/@ElementCollection for values
Inheritance (strongest)extends; JPA @Inheritance
Table 7 — Expressing relationships
JAVA
// Composition of value objects: embedded in the owner's table
@Embeddable public record Address(String street, String city, String postcode) { }
 
@Entity class Customer {
    @Embedded private Address billingAddress;
    @ElementCollection private List<Address> deliveryAddresses = new ArrayList<>();
}

6Practical Examples

6.1 Classifying relationships in a domain

PairRelationshipReason
Order — OrderLineCompositionLines belong to exactly one order and die with it
Order — CustomerAssociation by IDCustomer lives independently; different aggregate
Playlist — TrackAggregationTracks exist independently and are shared
InvoiceService — ClockDependency (or association if injected)Used during an operation
Order — AddressComposition of a valueCopy of the delivery address at order time
Table 8 — Domain relationship analysis

6.2 Reducing association to dependency

JAVA
// Association: service stores a collaborator it only needs in one method
class ExportService {
    private final CsvWriter writer = new CsvWriter();
    byte[] export(List<Row> rows) { return writer.write(rows); }
}
 
// Dependency: collaborator passed where needed (useful when it varies per call)
class ExportService {
    byte[] export(List<Row> rows, RowWriter writer) { return writer.write(rows); }
}

6.3 Relationship review checklist

  • Is each association unidirectional unless both directions are required?
  • Do references across aggregates/modules use IDs?
  • Are composed parts created and modified only through the whole?
  • Are JPA cascades used only for composition?
  • Could any stored reference be a method-level dependency instead?

7Common Problems & Mistakes

  • Object graphs spanning aggregates, loading half the database.
  • Bidirectional associations out of sync.
  • Cascade on associations between independent entities (deleting a customer's shared data).
  • Exposing composed parts for external modification.
  • Hard-wired dependencies (new, static calls) where injection is needed.
  • Over-debating aggregation vs association in UML while ignoring lifecycle.

8Performance & Production Considerations

AspectConsideration
Lazy loadingAssociations mapped as object references can trigger N+1 queries
Aggregate sizeLarge compositions load and lock more data per transaction
MemoryLong-lived associations keep object graphs reachable (leak risk in caches)
ConcurrencySmall aggregates reduce lock contention and optimistic-locking conflicts
Table 9 — Runtime implications

In production, relationship choices mostly show up in the persistence layer. Map @ManyToOne associations as FetchType.LAZY, because JPA makes them eager by default, and load the graph a use case needs with fetch joins or @EntityGraph instead of navigating lazily inside loops, which causes N+1 queries. Hibernate statistics or a query-count assertion in integration tests catch regressions early. Keep compositions small: an aggregate with thousands of children loads, locks and versions all of them on every change, so a very large collection usually signals a missing aggregate. Avoid cascades across aggregates, which can delete or update far more rows than intended. In caches, store IDs or small view records rather than entity graphs, which keep large object trees reachable and inflate heap usage.

9Architecture & Design Considerations

  • Composition = consistency boundary; associations by ID = boundaries between aggregates, modules and services.
  • Service data ownership follows the same rule: services reference each other's entities by ID only.
  • Relationship strength guides modularity: strong relationships belong inside a module; weak ones cross boundaries.
  • UML is a communication tool: use relationship notation consistently in design documents.

10Interview Questions & Answers

Q1BasicConcept

What is the difference between association and dependency?

An association is a structural, long-lived relationship: one object holds a reference to another, or to its identity, in a field, so the link outlives a single operation and has a direction and a multiplicity. A dependency is temporary use: the class needs the other type only during an operation, as a parameter, local variable, return type or static call, and keeps nothing afterwards. ReminderService storing a Mailer field is an association; issue(Order order, Clock clock) using the Clock only for that call is a dependency. In UML, association is a solid line and dependency a dashed arrow. Dependency is weaker, so prefer it when the collaborator is only needed occasionally.

Q2BasicConcept

What is the difference between aggregation and composition?

Both are whole-part relationships, but they differ in who owns the lifecycle. In aggregation the parts exist independently of the whole and can be shared: players exist without a team and can move to another one, and tracks can appear in many playlists. In composition the whole owns its parts: it creates them, controls all changes to them and deletes them with itself, and each part belongs to exactly one whole at a time, like order lines in an order. In Java, composition usually means a private collection populated only by the whole's methods; in JPA it means cascading and orphan removal. UML marks aggregation with a hollow diamond and composition with a filled one.

Q3IntermediateCode

How is composition expressed in JPA?

For child entities, map the collection on the whole as @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true). Cascading means persisting, merging or removing the Order also applies to its OrderLine rows, and orphan removal deletes a line as soon as it is removed from the collection, so no row survives without its parent. For value objects without identity, use @Embedded for a single value such as a billing Address, stored in the owner's table, and @ElementCollection for a collection of values, stored in a table owned by the entity. Keep the collection private and expose methods such as addLine, so parts change only through the whole in code too.

Q4IntermediateTrade-off

Why prefer unidirectional associations?

A bidirectional association couples both classes to each other: each must know the other's type, and both sides must be kept consistent on every change. In JPA that means an @OneToMany with mappedBy plus an @ManyToOne, and code that sets line.order whenever a line is added; forget it and the in-memory model disagrees with what is persisted. Unidirectional associations halve that coupling, make ownership obvious and avoid accidental navigation into large object graphs, which also reduces lazy-loading surprises and serialisation cycles in JSON. Choose bidirectional only when navigation in both directions is a real, frequent need, and then hide the synchronisation inside helper methods on the owning side.

Q5Senior EngineerDesign

Why should aggregates reference each other by ID?

An aggregate is a consistency boundary, and referencing another aggregate by ID keeps that boundary visible in code. With an object reference, loading an Order can pull in the Customer and everything it references, causing lazy-loading traps, N+1 queries and huge graphs. It also tempts developers to change two aggregates in one transaction, which increases locking and optimistic-locking conflicts. Holding a CustomerId makes the dependency explicit: if the use case needs customer data, it loads the customer through its own repository. ID references also decouple persistence mappings, so each aggregate can be stored and evolved independently, and they still work when the other aggregate later moves into a separate service.

Q6Senior EngineerConcept

What is the weakest relationship, and why prefer it?

Dependency is the weakest relationship, because the class uses the collaborator only during one operation and keeps no reference afterwards. That means no long-lived state to keep consistent, nothing holding other objects reachable in memory, and a collaborator that can differ on every call, which suits things like a Clock, a RowWriter or a pricing context. Coupling is lowest when the parameter is an interface type and higher when the method calls new on a concrete class or a static method. Preferring the weakest relationship that still expresses the domain keeps classes easy to test and change. Promote a dependency to an association only when the collaborator is genuinely needed across many operations.

Q7Technical ArchitectDesign

How do relationships influence module boundaries?

Relationship strength tells you what must stay together and where you can cut. Composition clusters, such as an order with its lines, share invariants and lifecycle, so they belong in one aggregate and therefore one module. Strong object associations between classes suggest they change together and also belong together. Associations by ID, and especially collaboration through events, are weak; they mark natural seams where a module boundary, and later perhaps a service boundary, can sit. When designing modules, I classify relationships first, keep strong ones inside a module, replace object references that cross modules with IDs or published views, and enforce the resulting dependency direction with ArchUnit or Spring Modulith.

Q8Solution ArchitectDesign

How do you model relationships between entities owned by different services?

Across services, the only shared reference is an identifier. The Order service stores a CustomerId, never a Customer object graph, and there are no foreign keys or joins across service databases, because each service owns its data. When a service needs data owned by another, it either calls that service's API on demand, which adds runtime coupling, or keeps a local, read-only copy of just the fields it needs, updated from events such as CustomerRenamed. That copy is eventually consistent, so the design must tolerate staleness, and values such as a delivery address are often snapshotted at order time anyway. Composition and cascades stay strictly inside one service's aggregates.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 5

Software Design & Design Patterns

Volume I · Topic 1.1

Software Design

Software Design Foundations & Principles

Volume I · Topic 1.2

Why Software Design Matters

Software Design Foundations & Principles

Volume I · Topic 1.3

Good vs Poor Design

Software Design Foundations & Principles

Volume II · Topic 1.1

Objects

Object-Oriented Design & SOLID

Volume II · Topic 1.2

Classes

Object-Oriented Design & SOLID

Volume III · Topic 1.1

What Is a Design Pattern?

Creational & Structural Design Patterns

Volume III · Topic 1.2

Pattern Categories

Creational & Structural Design Patterns

Volume III · Topic 1.3

Pattern vs Framework

Creational & Structural Design Patterns

Volume IV · Topic 1.1

Strategy Pattern

Behavioral Design Patterns

Volume IV · Topic 1.2

Strategy in Java

Behavioral Design Patterns

Volume IV · Topic 1.3

Strategy vs Conditional Logic

Behavioral Design Patterns

Volume V · Topic 1.1

Layered Architecture

Enterprise & Application Design Patterns

Volume V · Topic 1.2

Service Layer

Enterprise & Application Design Patterns

Volume V · Topic 1.3

Repository

Enterprise & Application Design Patterns

Volume VI · Topic 1.1

What Is Refactoring?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.2

Why Refactor?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.3

Refactoring Safety

Refactoring, Code Quality & Evolutionary Design

Volume VII · Topic 1.1

Method-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.2

Class-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.3

Module-Level Design

Advanced Design & Architecture-Level Design Decisions