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

📚 Interview question

What is composition in object-oriented design? Show how a C# class can be built from smaller objects that it owns and delegates to, and explain the benefits for change, testing and reuse.

Composition as building objects from owned parts and delegating to them, with strategies and decorators as common forms.

Mid-Level Theory Intermediate composition decorator-pattern delegation oop strategy-pattern

🎯 Key points

  • Composition builds a class from parts it holds and delegates to (has-a)
  • In strict UML terms composition means the whole owns the parts' lifetime
  • Strategies make behaviour swappable; decorators add behaviour around the same interface
  • Decorators combine freely, avoiding a subclass per feature combination
  • Composition avoids fragile base classes and makes parts testable in isolation
  • Do not inherit from collections; hold one privately and expose what is needed
  • The creator of a disposable part disposes it; injected parts are owned by the container
  • .NET itself uses composition widely: streams, HTTP handlers, middleware

⏱ 30-second 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.

🗣 Full answer (senior)

How I would answer this in an interview

Composition means building a class from smaller objects it holds and delegates to, instead of inheriting behaviour. The outer object controls the collaboration and exposes its own focused API; the parts each do one job. It's the default way to reuse and vary behaviour in modern C# — "has-a" rather than "is-a" — and it pairs naturally with dependency injection.

1. An example

public sealed class InvoiceGenerator(
    ITaxCalculator tax,
    IInvoiceNumberGenerator numbers,
    IInvoiceRenderer renderer,
    TimeProvider clock)
{
    public async Task<InvoiceDocument> GenerateAsync(Order order, CancellationToken ct)
    {
        var lines = order.Lines.Select(l => new InvoiceLine(l.Description, l.Amount, tax.For(l))).ToList();
        var number = await numbers.NextAsync(ct);
        var invoice = new Invoice(number, clock.GetUtcNow(), order.Customer, lines);
        return await renderer.RenderAsync(invoice, ct);   // PDF, HTML... delegated
    }
}

InvoiceGenerator owns the workflow; tax rules, numbering, rendering and time are separate collaborators. Each can be swapped, tested and changed independently.

Composition also applies inside the domain: an Order owns its OrderLines and an Address value object — they're part of the order and live and die with it.

2. Delegation

The outer class forwards work to its parts, sometimes adding behaviour around them:

public sealed class CachingProductCatalog(IProductCatalog inner, IMemoryCache cache) : IProductCatalog
{
    public Task<Product?> FindAsync(string sku, CancellationToken ct) =>
        cache.GetOrCreateAsync($"product:{sku}", e =>
        {
            e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            return inner.FindAsync(sku, ct);
        });
}

That's the decorator pattern — adding caching without modifying or inheriting from the real catalog. ASP.NET Core's middleware pipeline, DelegatingHandlers for HttpClient, and Polly resilience pipelines are all composition.

3. Benefits

Change

  • Variation is achieved by swapping parts, not by editing a base class every subclass depends on.
  • Independent dimensions (format × delivery × schedule) combine freely without a class per combination.
  • Behaviour can even be selected at run time (strategy chosen by configuration or tenant).

Testing

  • Each part has a narrow responsibility and can be unit-tested alone.
  • The composite can be tested with fakes for boundaries (FakeTimeProvider, an in-memory renderer).
  • No need to construct a whole inheritance chain to test one behaviour.

Reuse

  • Parts are reusable in other compositions (ITaxCalculator used by quotes, invoices and refunds).
  • Coupling is only to the parts' public contracts, not to a base class's internals (no fragile base class problem).

4. Costs

  • More types and wiring (DI registrations).
  • Indirection when reading code — you follow interfaces to implementations.
  • Forwarding boilerplate when you need most of another type's API (C# has no built-in delegation syntax).

These are usually worth it; for genuinely hierarchical, stable is-a relationships, inheritance can still be simpler.

5. A production example

A reporting service had ReportBase with subclasses for each report × output format × delivery channel — 37 classes. Adding "deliver to SharePoint" required touching a dozen. We recomposed it as Report = IReportQuery + IReportFormatter + IReportDelivery, configured per report in a table. The class count dropped, new channels became one class, and tests for formatters no longer needed database fixtures.

🔍 What the interviewer is checking

Checks whether the candidate can design flexible classes without deep hierarchies.

  • Green flags: explains has-a and delegation, ownership in strict composition, strategy and decorator examples, class explosion avoided, and disposal ownership.
  • Red flags: cannot give a composition example, inherits collections, or confuses composition with any field reference.

📚 Learn in depth

1. Composition in UML terms

In UML, composition (filled diamond) is the strongest form of association: the whole owns the part, and the part's lifetime depends on the whole (an order line can't exist without its order). Aggregation (hollow diamond) is a weaker "has-a" where parts can be shared or outlive the whole. In everyday design discussions, "composition over inheritance" uses composition more loosely to mean "assemble behaviour from collaborating objects" — including services injected by DI, which the composite doesn't own exclusively.

2. Patterns built on composition

Pattern Idea .NET example
Strategy Swap an algorithm behind an interface IComparer<T>, IPasswordHasher<T>
Decorator Wrap to add behaviour, same interface BufferedStream, DelegatingHandler, caching repositories
Composite Treat a group like a single item CompositeChangeToken, composite validators
Chain of responsibility Pass request through handlers ASP.NET Core middleware, MediatR pipeline behaviours
Adapter Wrap to change an interface Wrapping a vendor SDK behind IPaymentGateway
Facade Simplify a set of collaborators InvoiceGenerator above

3. Decorators with DI

The built-in container doesn't support decoration directly; options:

  • Register manually: services.AddScoped<IProductCatalog>(sp => new CachingProductCatalog(sp.GetRequiredService<SqlProductCatalog>(), sp.GetRequiredService<IMemoryCache>()));
  • Use Scrutor's services.Decorate<IProductCatalog, CachingProductCatalog>().
  • Use keyed services (.NET 8) to distinguish inner and outer registrations.

4. Ownership and lifetimes

When composing, decide who owns each part:

  • Parts created by the composite (new OrderLine(...)) are owned — dispose them if disposable.
  • Parts injected by DI are owned by the container — don't dispose them, and respect lifetimes: a singleton composing a scoped service causes a captive dependency (the scoped service lives forever, e.g. a DbContext shared across requests). ValidateScopes in Development catches this.

5. Composition in the domain model

public sealed class Order
{
    private readonly List<OrderLine> _lines = [];        // owned parts
    public ShippingAddress Address { get; private set; } // value object, owned
    public CustomerId CustomerId { get; }                // reference by ID to another aggregate (not owned)
}

In DDD terms, an aggregate is a composition boundary: the root owns its parts, enforces invariants across them, and is persisted as a unit. Other aggregates are referenced by ID, not composed. EF Core models owned parts with owned types or ComplexProperty (EF Core 8) for value objects, and cascade deletes for owned children.

6. Delegation boilerplate

When wrapping a large interface (e.g. Stream), you must forward many members. Options: inherit from a base that forwards (Stream wrappers often derive from Stream and forward), source generators that implement forwarding, or design smaller interfaces so wrappers stay small (Interface Segregation helps composition).

Try it yourself

  1. Write a decorator that adds logging and timing to an interface, and register it with Scrutor or manually.
  2. Refactor an inheritance-based variation in your code into a strategy injected via DI.
  3. Turn on ValidateScopes/ValidateOnBuild and look for captive dependencies.
  4. Map an owned value object with EF Core's ComplexProperty and observe the table columns.

💻 Code

public interface INotifier
{
    Task SendAsync(string to, string message, CancellationToken ct);
}

public sealed class EmailNotifier(IEmailClient client) : INotifier
{
    public Task SendAsync(string to, string message, CancellationToken ct) =>
        client.SendAsync(to, "Notification", message, ct);
}

// Decorators: same interface, wrap another INotifier
public sealed class RetryingNotifier(INotifier inner, int attempts = 3) : INotifier
{
    public async Task SendAsync(string to, string message, CancellationToken ct)
    {
        for (int i = 1; ; i++)
        {
            try { await inner.SendAsync(to, message, ct); return; }
            catch (HttpRequestException) when (i < attempts)
            {
                await Task.Delay(TimeSpan.FromMilliseconds(200 * i), ct);
            }
        }
    }
}

public sealed class LoggingNotifier(INotifier inner, ILogger<LoggingNotifier> log) : INotifier
{
    public async Task SendAsync(string to, string message, CancellationToken ct)
    {
        log.LogInformation("Sending notification to {To}", to);
        await inner.SendAsync(to, message, ct);
    }
}

// Composition with owned parts (strict UML composition): lines live and die with the order
public sealed class Order
{
    private readonly List<OrderLine> _lines = [];        // owned, not shared
    public IReadOnlyList<OrderLine> Lines => _lines;
    public void AddLine(string sku, int qty) => _lines.Add(new OrderLine(sku, qty));
}

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

// Wiring the chain
INotifier notifier = new LoggingNotifier(
    new RetryingNotifier(new EmailNotifier(emailClient)), logger);

⚠ Common mistakes

1. Using inheritance for variation

What happens: each variant is a subclass, especially across multiple dimensions. Why it is wrong: combinatorial class explosion and fragile base classes. Do instead: compose independent parts (strategies) for each dimension.

2. Composing with concrete types

What happens: a class news up its collaborators internally (new SmtpClient()). Why it is wrong: hard to test or swap; hidden dependencies. Do instead: inject collaborators through the constructor behind interfaces where variation or testing matters.

3. God composites

What happens: one class composes fifteen services. Why it is wrong: too many responsibilities; constructor over-injection is a design smell. Do instead: split into smaller composites or introduce facades for cohesive groups.

4. Captive dependencies

What happens: a singleton composes a scoped DbContext. Why it is wrong: the context is shared across requests — thread-safety bugs and stale data. Do instead: match lifetimes, or inject IServiceScopeFactory/factories in singletons.

5. Disposing injected parts

What happens: a composite disposes services it received from DI. Why it is wrong: the container owns them; other consumers then use disposed objects. Do instead: dispose only what you create.

6. Leaky decorators

What happens: a caching decorator changes semantics (returns stale data where callers need fresh, swallows exceptions). Why it is wrong: violates the interface contract callers rely on. Do instead: document and preserve semantics; make staleness explicit.

7. Composing everything by ID or everything by reference

What happens: aggregates hold object references to other aggregates (Order.Customer fully loaded). Why it is wrong: blurs ownership and transactional boundaries, encourages huge object graphs. Do instead: compose owned parts by reference; reference other aggregates by ID.

8. Over-abstracting every part

What happens: every tiny helper gets an interface and DI registration. Why it is wrong: wiring noise without benefit. Do instead: inject boundaries and real variation points; use plain classes or static pure functions for the rest.

🏛 Senior insight: trade-offs

Seniors reach for composition by default because it localises change, and they use the right composition pattern for the axis of variation.

Axis of change Composition tool
Algorithm varies strategy interface or delegate
Cross-cutting concerns (retry, cache, log) decorators
Ordered processing pipeline / middleware
Parts owned by an aggregate private fields, created inside
Parts provided by infrastructure constructor injection

They also know composition is how .NET itself is built: Stream decorators, HttpMessageHandler chains, middleware and IOptions all favour has-a over is-a.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says prefer composition Why: Checks they know when inheritance is still right
    How to answer

    Inheritance lets a derived class reuse and specialise a base class: class SavingsAccount : Account. C# allows one base class but many interfaces. The derived class inherits all members except constructors and finalisers (private ones exist but are not accessible), can call base members with base., and can override virtual/abstract members. Declaring a member with the same name without override hides it (new), and which one runs then depends on the variable's static type, a common source of bugs. Use inheritance only for a real is-a relationship where a derived object can be used anywhere the base is expected (Liskov substitution). Deep hierarchies, inheriting just to reuse code, and fragile base classes are the main problems; composition is often better.

  2. When: Candidate says the class owns its parts Why: Probes composition versus aggregation lifetimes
    How to 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.

🛠 Real-world problems: how you handle them

Notifier class hierarchy exploded with feature combinations
Situation
A notification service used inheritance for channels and features: `EmailNotifier`, `SmsNotifier`, `RetryingEmailNotifier`, `LoggedRetryingSmsNotifier` and more, [nine] classes in total.
Signals
Adding push notifications required [four] new classes; a bug fix to retry logic had to be copied into three places and one was missed.
Investigation
Mapped the hierarchy and saw two independent axes (channel and cross-cutting features) forced into one inheritance tree.
Resolution
Defined `INotifier`, kept one class per channel, and turned retry and logging into decorators. DI registers the chain per channel.
Prevention
Design guideline to model cross-cutting behaviour as decorators, plus unit tests for each decorator with fake inner notifiers.

How to tell it (STAR)

**Situation:** At [company] our notification code had nine classes for combinations of channel, retry and logging, and adding push notifications meant writing four more. **Task:** I wanted to add the new channel without growing the hierarchy, and fix a retry bug once. **Action:** I saw two independent axes squeezed into one inheritance tree. I introduced an `INotifier` interface, kept one class per channel, and moved retry and logging into decorators that wrap any notifier. DI composes the chain. I wrote unit tests for each decorator. **Result:** Nine classes became four, push notifications needed one new class, and the retry fix applied to every channel automatically.

↩ Usually asked after

More in Core OOP

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me