📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What is aggregation, and how does it differ from composition? Use examples to explain shared versus owned parts, lifetimes, deletion and cascade rules, and how the difference shows up in C# code and EF Core mappings.

Aggregation versus composition: whole/part relationships with shared or owned parts, and the consequences for lifetimes, deletes and persistence.

Mid-Level Theory Intermediate aggregation composition ef-core oop uml

🎯 Key points

  • Aggregation and composition are both whole/part associations
  • Aggregation: parts exist independently, can be shared and can move between wholes
  • Composition: the whole exclusively owns its parts, which live and die with it
  • Composition implies cascade delete; aggregation must not cascade
  • Composed parts are created and changed only through the whole
  • Aggregated parts are referenced, often by ID, and managed elsewhere
  • EF Core: owned types or required FK with cascade for composition; optional FK with restrict or set-null for aggregation
  • In DDD, composition sits inside an aggregate; aggregation links aggregates

⏱ 30-second answer

Both are whole/part associations. In aggregation the parts exist independently and can be shared or move between wholes: a Team has Players, but a player exists without the team and can transfer. In composition the whole owns its parts exclusively; they are created with it and destroyed with it: an Order and its OrderLines. The practical consequences: composition implies cascade delete, parts are created inside the whole and not exposed for outside modification; aggregation implies no cascade, parts are referenced (often by ID) and managed elsewhere. In EF Core, composition maps naturally to owned types or required relationships with cascade delete; aggregation maps to optional or many-to-many relationships without cascade.

πŸ—£ Full answer (senior)

How I would answer this in an interview

Both are "has-a" relationships. The difference is ownership and lifetime: with composition, the whole owns its parts β€” they can't exist without it and are deleted with it. With aggregation, the whole uses parts that have independent lifetimes and may be shared. The distinction drives deletion rules, cascade configuration and how you write the C# and EF Core model.

1. Examples

Whole Part Relationship Why
Order OrderLine Composition A line means nothing without its order; delete the order β†’ delete lines
Order ShippingAddress (value object) Composition The address snapshot belongs to the order
Invoice InvoiceLine Composition Same as order lines
Team Player Aggregation A player exists independently and can move teams
Course Student Aggregation (many-to-many) Students exist without courses and attend many
Playlist Song Aggregation Songs are shared by many playlists
Department Employee Aggregation Deleting a department shouldn't delete employees

2. Shared vs owned parts

  • Owned (composition): exactly one owner at a time; the part isn't referenced from elsewhere; the owner controls creation and changes (order.AddLine(...)).
  • Shared (aggregation): the part can be referenced by many wholes; the whole doesn't control the part's lifecycle; changes to the part (a song's title) are visible to all wholes.

3. Lifetimes, deletion and cascade rules

  • Composition β†’ deleting the whole deletes the parts: DeleteBehavior.Cascade. Removing a line from the collection should delete it (an "orphan").
  • Aggregation β†’ deleting the whole must not delete the parts: Restrict/NoAction (block deletion while referenced) or SetNull (detach). For many-to-many, only the join rows are deleted.

Getting this wrong is expensive: cascade on an aggregation (deleting a department deletes its employees), or no cascade on a composition (orphaned OrderLine rows, FK errors).

4. In C# code

// Composition: owner creates and controls the parts
public sealed class Order
{
    private readonly List<OrderLine> _lines = [];
    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
    public ShippingAddress Address { get; private set; }

    public void AddLine(ProductId product, int qty, Money price) => _lines.Add(new OrderLine(product, qty, price));
    public void RemoveLine(OrderLineId id) => _lines.RemoveAll(l => l.Id == id);
}

// Aggregation: whole references independent objects (often by ID across aggregates)
public sealed class Team
{
    private readonly HashSet<PlayerId> _players = [];
    public IReadOnlySet<PlayerId> Players => _players;
    public void Sign(PlayerId player) => _players.Add(player);
    public void Release(PlayerId player) => _players.Remove(player);   // player still exists
}

Signals of composition in code: the part's constructor is internal/called only by the owner, parts are private collections, and they're value objects or child entities without their own repository. Aggregation: parts have their own repository/lifecycle and are referenced by ID or shared reference.

5. In EF Core mappings

// Composition: child entities with cascade, or owned/complex types
modelBuilder.Entity<Order>().HasMany(o => o.Lines).WithOne().OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Order>().ComplexProperty(o => o.Address);        // EF Core 8: value object columns in Orders table
// or OwnsOne(o => o.Address) / OwnsMany(o => o.Lines) for owned entity types

// Aggregation: independent entities, no cascade
modelBuilder.Entity<Employee>()
    .HasOne<Department>().WithMany().HasForeignKey(e => e.DepartmentId)
    .OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Course>().HasMany(c => c.Students).WithMany(s => s.Courses);   // join rows deleted only

Owned types (OwnsOne/OwnsMany) and complex types (EF Core 8) express composition directly: the part has no independent identity in the model and is always loaded and saved with its owner. EF Core also deletes orphans of required relationships when you remove a child from a tracked collection.

6. A production example

An HR system mapped Department β†’ Employees with EF Core's default cascade for a required relationship. A manager deleted an empty-looking test department in production that actually had employees assigned by an import β€” and cascade deleted 140 employee records, along with their leave balances. We restored from point-in-time backup, changed the relationship to Restrict with soft-deletes for departments, and added a review step for every OnDelete(Cascade) in migrations. The bug was modelling an aggregation as a composition.

πŸ” What the interviewer is checking

Checks precise modelling knowledge with real persistence consequences.

  • Green flags: clear shared versus owned distinction, examples of each, lifetime and cascade rules, EF Core owned types versus restrict/set-null, DDD aggregate link.
  • Red flags: uses the terms interchangeably, cascades everything, or exposes composed collections publicly.

πŸ“š Learn in depth

1. UML definitions

  • Aggregation (hollow diamond β—‡): a whole–part association where the part can exist independently and be shared. UML itself treats aggregation as semantically weak β€” many modellers avoid it and just use association.
  • Composition (filled diamond β—†): strong ownership; a part belongs to at most one whole at a time, and if the whole is destroyed, so are its parts (unless explicitly moved first).

In practice, the useful question is: "If I delete the whole, should the parts be deleted?" and "Can the part be referenced by anything else?"

2. DDD aggregates (a different use of "aggregate")

In domain-driven design, an aggregate is a cluster of objects treated as one unit for changes and consistency, with a single aggregate root (e.g. Order with its OrderLines). Internally that's composition; between aggregates, references are by ID, which is closer to UML aggregation/association. The naming collision causes confusion in interviews β€” worth clarifying.

Rules of thumb for aggregate design (Vaughn Vernon): keep aggregates small, reference other aggregates by ID, modify one aggregate per transaction, and use domain events for eventual consistency across aggregates.

3. EF Core delete behaviours

DeleteBehavior Database FK action Tracked dependents Use for
Cascade ON DELETE CASCADE Deleted Composition (owned children)
Restrict NO ACTION (fails if referenced) Error if loaded Aggregation where deletion must be blocked
NoAction NO ACTION Not changed by EF Same, letting DB enforce
SetNull ON DELETE SET NULL FK set to null Optional aggregation (detach)
ClientSetNull NO ACTION FK set to null in memory Default for optional relationships
ClientCascade NO ACTION Deleted in memory only When DB can't cascade (multiple paths)

EF Core's defaults: required relationships default to Cascade; optional ones to ClientSetNull. So a non-nullable FK (e.g. int DepartmentId) silently gives you cascade β€” exactly the HR bug above. SQL Server also rejects multiple cascade paths, forcing explicit choices.

4. Orphan removal and owned types

  • Removing a child from a tracked required collection makes it an orphan; EF Core deletes it on SaveChanges (configurable via DeleteOrphansTiming).
  • Owned entity types (OwnsOne, OwnsMany): no DbSet, can't be queried independently, always loaded with the owner, stored in the owner's table (OwnsOne) or a separate table (OwnsMany).
  • Complex types (EF Core 8+): value-object semantics with no identity, stored as columns of the owner; better for immutable value objects (records).

5. Soft deletes and aggregation

For aggregated parts with history (employees, customers), soft deletes (IsDeleted + global query filter) avoid destructive cascades and preserve audit trails. Composition parts usually go with their owner whether soft or hard deleted. Note: privacy obligations (APP 11 β€” destroy or de-identify personal information no longer needed) may require real deletion eventually; plan retention jobs.

6. Testing ownership rules

  • Domain tests: removing a line from an order removes it from Lines and updates totals.
  • Persistence tests (against a real SQL Server via Testcontainers): deleting an order deletes its lines; deleting a department with employees fails.
  • Migration review: grep generated migrations for onDelete: ReferentialAction.Cascade.

Try it yourself

  1. Map Department–Employee with a non-nullable FK and inspect the generated migration's onDelete.
  2. Change to Restrict, try deleting a department with employees, and observe the error.
  3. Model Order.Address as ComplexProperty and Order.Lines with OwnsMany, and compare tables.
  4. Remove a line from a tracked order and check the SQL emitted on SaveChanges.

πŸ’» Code

// Composition: Order owns its lines
public sealed class Order
{
    private readonly List<OrderLine> _lines = [];
    public Guid Id { get; } = Guid.NewGuid();
    public IReadOnlyList<OrderLine> Lines => _lines;

    public void AddLine(string sku, int quantity, decimal unitPrice)
    {
        if (_lines.Sum(l => l.Quantity) + quantity > 100)
            throw new InvalidOperationException("An order cannot exceed 100 items.");  // rule across parts
        _lines.Add(new OrderLine(sku, quantity, unitPrice));                           // created by the whole
    }
}

public sealed record OrderLine(string Sku, int Quantity, decimal UnitPrice);

// Aggregation: Team groups independent players by ID
public sealed class Team(string name)
{
    private readonly HashSet<Guid> _playerIds = [];
    public string Name { get; } = name;
    public IReadOnlyCollection<Guid> PlayerIds => _playerIds;

    public void Sign(Player player) => _playerIds.Add(player.Id);   // links an existing object
    public void Release(Player player) => _playerIds.Remove(player.Id);
}

public sealed class Player(string name)
{
    public Guid Id { get; } = Guid.NewGuid();
    public string Name { get; } = name;
}

// EF Core configuration showing the difference
public sealed class AppDb(DbContextOptions<AppDb> options) : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder b)
    {
        b.Entity<Order>().OwnsMany(o => o.Lines);            // composition: no DbSet<OrderLine>, deleted with order

        b.Entity<Employee>()
            .HasOne<Department>().WithMany()
            .HasForeignKey(e => e.DepartmentId)
            .OnDelete(DeleteBehavior.Restrict);              // aggregation: never cascade
    }
}

public sealed class Department { public int Id { get; set; } public string Name { get; set; } = ""; }
public sealed class Employee { public int Id { get; set; } public int? DepartmentId { get; set; } }

⚠ Common mistakes

1. Treating aggregation as composition

What happens: cascade deletes configured from department to employees or customer to orders. Why it is wrong: deleting the whole destroys independent data. Do instead: use Restrict/SetNull or soft deletes for parts with independent lifetimes.

2. Relying on EF Core default delete behaviour

What happens: required FKs default to cascade without anyone deciding. Why it is wrong: destructive behaviour by accident. Do instead: configure OnDelete explicitly for every relationship and review migrations.

3. Giving owned parts their own repositories

What happens: OrderLineRepository allows editing lines without the order. Why it is wrong: bypasses the owner's invariants (totals, status rules). Do instead: modify parts only through the owner.

4. Sharing parts that should be owned

What happens: two orders reference the same Address entity; updating one changes the other. Why it is wrong: historical data (where an order was shipped) changes retroactively. Do instead: copy value objects into each owner (snapshot), modelled as complex/owned types.

5. Leaving orphans

What happens: composition modelled without cascade; removed children stay in the database. Why it is wrong: orphan rows, FK errors and inconsistent totals. Do instead: cascade deletes and orphan removal for owned children.

6. Confusing DDD aggregates with UML aggregation

What happens: "aggregate" used ambiguously in design discussions. Why it is wrong: leads to inconsistent modelling decisions. Do instead: clarify: DDD aggregates are consistency boundaries (composition inside, ID references outside).

7. Loading entire aggregations

What happens: Include(t => t.Players) everywhere for teams with large rosters. Why it is wrong: loads independent objects the use case doesn't need. Do instead: reference by ID and query as needed.

8. Hard-deleting aggregated entities with history

What happens: deleting employees or customers referenced by historical records. Why it is wrong: breaks audit trails and reports, or fails on FKs. Do instead: soft-delete with retention policies that respect privacy obligations.

πŸ› Senior insight: trade-offs

Seniors use the distinction to set transaction and consistency boundaries, not just to draw diagrams.

Concern Composition Aggregation
Transaction whole and parts saved together separate transactions
Invariants enforced by the whole enforced by each object or a process
Delete cascade restrict / reassign
Loading always with the whole on demand, by ID
DDD inside one aggregate between aggregates

The senior point: wrong cascade rules are one of the few modelling errors that destroy data, so the aggregation versus composition decision should be explicit and reviewed.

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate defines composition as ownership Why: Checks composition as a design technique, not only UML
    How to answer

    Composition means a class has other objects as parts and delegates work to them, instead of inheriting behaviour. A Car has an Engine and Wheels; an OrderProcessor has a validator, a pricer and a notifier. In the strict UML sense, composition also means ownership: the parts are created and destroyed with the whole and are not shared. Benefits: behaviour can be swapped (pass a different IPricingStrategy), combined freely (decorators wrap each other), tested in isolation, and changed without a fragile base class. The cost is a little more wiring, usually handled by constructors or dependency injection.

  2. When: Candidate mentions navigation properties Why: Probes multiplicity and direction of associations
    How to answer

    Association is a lasting structural relationship where one object knows about another and can reach it, usually through a field or property: a Customer places Orders, a Teacher teaches Courses. It is the general relationship; aggregation and composition are stronger special cases that add ownership rules, and dependency is a weaker, temporary use. An association has multiplicity (one-to-one, one-to-many, many-to-many) and direction: unidirectional (Order.Customer only) or bidirectional (Customer.Orders and Order.Customer). In C# it appears as a reference property, a collection, or just an ID; in EF Core as navigation properties and foreign keys. Prefer unidirectional associations and IDs across aggregate boundaries; bidirectional links must be kept in sync and create cycles for serialisers.

πŸ›  Real-world problems: how you handle them

Cascade delete on Department removed employee records
Situation
An HR system mapped `Department` to `Employee` as a required one-to-many with cascade delete, treating employees as parts owned by the department.
Signals
After an admin deleted what looked like an unused department, [37] employee records disappeared; payroll export failed the next morning.
Investigation
Database logs showed a single delete cascading to the employee table. The EF Core model configured `DeleteBehavior.Cascade`, and the admin UI did not show linked employees.
Resolution
Restored the rows from backup, changed the relationship to an optional foreign key with `Restrict`, and added a domain rule requiring employees to be reassigned before a department can be deleted.
Prevention
A review checklist item for every cascade rule, integration tests that try deleting wholes with linked parts, and soft delete for organisational structures.

How to tell it (STAR)

**Situation:** At [company] deleting a department in our HR system silently deleted 37 employee records, and payroll failed the next day. **Task:** I led the recovery and the fix. **Action:** We restored the data from backup. The root cause was modelling: employees were mapped as composed parts of a department with cascade delete, but they are independent people who move between departments, which is aggregation. I changed the mapping to `Restrict`, added a rule that departments must be empty before deletion, and introduced soft delete. I also added a review step for every cascade rule. **Result:** No data was lost permanently, and in [the following year] no cascade-related incidents happened; the review step caught [two] other risky cascades.

↩ Usually asked after

More in Core OOP

Last updated 30 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me