Domain-Driven Design interviews at the architect level are less about vocabulary and more about judgment under ambiguity: where does this boundary actually go, how big should this aggregate be, and what do you do when the "correct" DDD answer conflicts with what the business needs shipped this quarter. Interviewers use DDD questions to probe whether a candidate has actually modeled a non-trivial domain, or has only read the blue book's glossary. This page works through bounded contexts, aggregate design, domain versus integration events, anti-corruption layers, context mapping, event storming and how DDD's tactical patterns map onto EF Core in a real .NET codebase.
Q1 What is a bounded context, and how do you identify boundaries in an existing monolith that was never designed with DDD in mind?#
Short answer: A bounded context is the boundary within which a particular domain model, and the ubiquitous language that describes it, stays consistent and unambiguous — outside that boundary, the same word can and often does mean something different; in a legacy monolith, you find these boundaries by looking for places where one word carries divergent meanings or divergent business rules depending on who is using it, not by looking at existing folder or namespace structure, which usually reflects technical layering rather than domain boundaries.
The classic tell is a shared noun with incompatible lifecycles: "Product" in a catalog module means something with a description, images and a price ready to display; "Product" in a shipping module means something with a weight, dimensions and a set of restricted destinations; "Product" in a finance module means something with a revenue recognition category. If a single Product class in the codebase has accumulated fields for all three concerns, that class is quietly straddling three bounded contexts, and changes for one concern risk regressions in the other two. Team structure is a second, practical signal — where Conway's Law already shows two teams making conflicting, hard-to-reconcile changes to what looks like "the same" entity, that friction is usually the boundary announcing itself, even if nobody has named it yet. Event storming, covered later on this page, is the structured way to surface these boundaries deliberately rather than waiting for them to show up as production incidents.
What interviewers look for: using linguistic divergence and team friction as concrete detection signals, rather than defining a bounded context only in the abstract.
Common mistakes: treating existing project or namespace boundaries as bounded contexts, when they were very often drawn along technical lines (Data, Services, Controllers) that cut straight through a real domain boundary.
Q2 How big should an aggregate be? Walk through the trade-offs of a too-large versus too-small aggregate using Order and OrderLine as an example.#
Short answer: An aggregate is a transactional consistency boundary, not a data-modeling convenience, so its size should be driven entirely by which invariants must be enforced atomically — Order should include OrderLine as part of the same aggregate because a rule like "the order total must always match the sum of its lines, and can't be saved in a partially updated state" can only be enforced within a single transaction; the general guidance, from Vaughn Vernon's "Effective Aggregate Design," is to keep aggregates as small as the invariants allow and reference other aggregates by identity, not by object reference.
A too-large aggregate — for example, folding Customer and every one of their Orders into one aggregate because "they're related" — creates a concurrency bottleneck: two unrelated operations, such as updating a customer's email address and placing a new order, now contend for the same optimistic concurrency token or row lock, producing conflicts that have nothing to do with a real business invariant being protected. A too-small aggregate has the opposite failure: splitting OrderLine out as its own independently-saveable aggregate makes it impossible to guarantee, within one transaction, that the order total stays consistent with its lines — you'd need eventual consistency and compensating logic for a rule that is fundamentally about atomic correctness, not eventual correctness. The practical rule of thumb: draw the aggregate boundary exactly around the smallest set of data that must change together, atomically, for the domain to remain valid, and reach for domain events and eventual consistency for everything else.
What interviewers look for: the invariant-first framing for sizing, plus concrete failure modes on both sides — contention for too-large, broken atomicity for too-small — rather than a vague "aggregates should be small."
Q3 What's an invariant in DDD terms, and show C# code for an aggregate that enforces one properly?#
Short answer: An invariant is a business rule that must always hold true for an aggregate to be in a valid state, and "enforcing it properly" means making the illegal state unrepresentable — no public setters or public collection mutators that let code bypass the rule, only methods and a constructor that guarantee the rule as a side effect of using the type at all, rather than a separate validation step that can be forgotten or skipped.
public sealed class Order
{
private readonly List<OrderLine> _lines = [];
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
public OrderStatus Status { get; private set; } = OrderStatus.Draft;
public void AddLine(ProductId productId, int quantity, decimal unitPrice)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot modify an order that is no longer a draft.");
if (quantity <= 0)
throw new DomainException("Order line quantity must be positive.");
_lines.Add(new OrderLine(productId, quantity, unitPrice));
}
public void Submit()
{
if (_lines.Count == 0)
throw new DomainException("Cannot submit an order with no lines.");
Status = OrderStatus.Submitted;
}
}Every path that could put Order into an invalid state — an order with a zero quantity line, or a submitted order with no lines — is closed off by the type itself: _lines is only reachable through AddLine, which validates before mutating, Lines exposes a read-only view so callers cannot bypass the guard by mutating the collection directly, and Status can only change through Submit, which checks its own precondition. This is meaningfully different from validating an already-mutable public model after the fact, such as a data annotation checked at the API boundary, because that style of validation only catches violations at one specific point in time and does nothing to stop code deeper in the domain from constructing an invalid state directly.
What interviewers look for: a concrete "always valid" design — private setters, guarded mutator methods, a read-only collection view — rather than describing invariants only in the abstract or relying on external validation.
Q4 Explain the difference between a domain event and an integration event, and why conflating them causes problems.#
Short answer: A domain event is an in-process signal that something significant happened inside a bounded context, expressed in the ubiquitous language of that context and typically consumed by handlers within the same process and deployment unit; an integration event crosses a bounded context or service boundary, needs a deliberately stable and versioned public contract, and is usually published to a message broker for other services to consume — conflating the two means publishing your internal domain model directly onto the wire, which couples every downstream consumer to your domain's internal shape and forces you to treat every internal refactor as a breaking external change.
The failure mode plays out predictably: a team defines OrderLineQuantityChanged as a domain event, finds it convenient to also publish it directly onto a message bus for other services, and now any internal refactor of the Order aggregate — renaming a field, splitting a value object, changing how quantity changes are represented — risks breaking every external consumer that deserializes that exact shape. The fix is a deliberate translation step at the boundary: domain events stay internal, and a small number of purpose-built integration events, often built from an outbox process (covered next), are the only things a bounded context exposes externally, versioned and evolved independently of whatever the internal domain model happens to look like this month. This is the same "publish a stable contract, not your internals" discipline API versioning already requires, applied to asynchronous messaging instead of HTTP.
What interviewers look for: naming the coupling failure specifically — internal refactors becoming external breaking changes — as the concrete cost of conflating the two, not just "they're different types of events."
Q5 What's the Outbox pattern, and why does it matter for reliably publishing events from an aggregate that also writes to a database via EF Core?#
Short answer: The Outbox pattern solves the dual-write problem — you cannot atomically both commit a database transaction and publish a message to a broker, because they are two separate systems with no shared transaction — by writing the event as a row in an "outbox" table inside the same database transaction as the aggregate's state change, then having a separate process read undelivered outbox rows and publish them, marking each as dispatched only after a successful send.
public class OrderSavingInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData, InterceptionResult<int> result)
{
var context = eventData.Context!;
var domainEvents = context.ChangeTracker.Entries<Order>()
.SelectMany(e => e.Entity.DequeueDomainEvents());
foreach (var domainEvent in domainEvents)
{
context.Add(new OutboxMessage(domainEvent));
}
return result;
}
}Because the outbox row and the aggregate's state change commit together as one EF Core transaction, either both persist or neither does — there is no window where the order is saved but the event is lost, or the event is sent but the order save later rolls back. A background service, or a change-data-capture process reading the outbox table, then delivers each message to the broker and marks it dispatched; because that delivery step can itself fail or be retried, downstream consumers must be idempotent, since the outbox pattern guarantees at-least-once delivery, not exactly-once. This is the standard, reliable way to bridge "transactional write to a relational database" with "asynchronous message to other services" without a distributed transaction coordinator.
What interviewers look for: identifying the dual-write problem by name as the reason a naive "save, then publish" approach is unsafe, and knowing that the outbox guarantees at-least-once delivery, which pushes an idempotency requirement onto consumers.
Q6 What is an Anti-Corruption Layer, and when would you introduce one versus just calling another bounded context's API directly?#
Short answer: An Anti-Corruption Layer (ACL) is a translation boundary that converts an external system's model, whether a legacy application, a partner API, or a third-party SaaS product, into your own bounded context's ubiquitous language, so that a foreign or poorly-designed external model never leaks into your domain; you introduce one when the external system's model is a poor fit, unstable, or outside your control, and you skip it when you're integrating with another well-designed bounded context whose team already publishes a stable, intentional contract you can consume directly.
The cost-benefit is genuinely two-sided: an ACL means writing and maintaining translation code — often a dedicated adapter and a set of mapping types — that exists purely to shield your domain, which is real, ongoing work with no feature-facing payoff of its own. That cost is worth paying when the alternative is your domain model gradually absorbing a legacy system's inconsistent field names, null-means-three-different-things semantics, or a partner API's breaking changes directly into your entities — the ACL is what lets your own domain stay clean and stable while the external system remains whatever it is. It is not worth paying between two internal bounded contexts on the same platform where the upstream team already treats their public contract as a first-class, versioned artifact; wrapping an already-stable, well-designed contract in another translation layer is pure ceremony with no corruption actually being prevented.
What interviewers look for: a clear decision rule based on the external model's quality and stability, not a blanket "always use an ACL at every integration point," which is itself an over-engineering answer.
Q7 Walk through the context mapping patterns and when you'd choose conformist versus building an Anti-Corruption Layer.#
Short answer: Context mapping names the relationships between bounded contexts: partnership (two teams coordinate as equals), shared kernel (two contexts deliberately share a small common model), customer-supplier (the downstream team has influence over the upstream team's roadmap), conformist (the downstream team has no influence and simply adopts the upstream model as-is), and open host service with published language (the upstream team exposes a deliberately stable, documented contract for many consumers) — you choose conformist when negotiating a better contract from the upstream team isn't realistic and their model is good enough to live with directly, and you build an ACL when the upstream model is actively harmful to your domain or you need strong isolation from its changes regardless of negotiating power.
The deciding factor is less about the pattern names and more about two independent questions: do you have any influence over the upstream team's model, and is that model good enough to consume as-is even if you can't change it? A downstream team integrating with a well-run internal platform team that publishes a genuinely well-designed API, but won't customize it per consumer, is a reasonable conformist relationship — adopting their model costs less than translating it, and the model itself isn't corrupting anything. A downstream team integrating with a legacy mainframe system with decades of inconsistent field semantics, where influence is impossible and the model itself is the problem, needs an ACL regardless of how the relationship is otherwise structured. Open host service with published language is the pattern the upstream team should aim for when it knows it has many consumers, since it front-loads contract design effort so individual downstream teams don't each need their own ACL.
What interviewers look for: using the two independent axes — influence over upstream, and quality of the upstream model — to choose a pattern, rather than treating the pattern list as a menu with no selection criteria.
Q8 What is Event Storming, and how do you run one to derive bounded contexts and aggregates from a business process nobody has fully mapped out?#
Short answer: Event Storming is a facilitated, workshop-style technique, developed by Alberto Brandolini, where domain experts and engineers collaboratively populate a large timeline with orange sticky notes for every significant domain event in past tense, such as "Order Placed" or "Payment Declined," then progressively add commands that trigger those events, the actors who issue them, and policies that react to events by triggering further commands — bounded contexts emerge as clusters of related events and commands that share vocabulary, and aggregates emerge as the specific entities that own the command-to-event transition within each cluster.
The "big picture" session starts unstructured — everyone in the room adds events without worrying about order or duplication — then the group physically arranges the notes into a rough timeline, resolves duplicate or conflicting language on the spot (which is itself valuable, since disagreement about what to call something is often a bounded-context boundary announcing itself), and only then adds command notes upstream of each event and actor notes identifying who issues each command. Clustering happens by looking for pivotal events, where language or ownership visibly shifts, and by grouping tightly related event-command chains together; each cluster is a candidate bounded context, and within a cluster, the entity that must exist to validate a command and produce the corresponding event consistently is a candidate aggregate. The facilitation details matter as much as the technique: the room needs actual domain experts, not just engineers guessing at the business process, and an unlimited supply of wall space, since the value comes from surfacing disagreement and gaps that a document or a meeting agenda would never have exposed.
What interviewers look for: knowing the technique is genuinely collaborative and expert-driven, not an engineering-only exercise, and being able to explain how bounded contexts and aggregates are derived from the resulting timeline rather than just describing sticky notes.
Q9 Where should aggregates and value objects live when using EF Core, and how do you keep persistence concerns from leaking into the domain model?#
Short answer: EF Core's complex types, introduced in EF Core 8, are the current recommended way to model DDD value objects: unlike owned entity types, a complex type has value semantics rather than reference/identity semantics, so the same instance can be assigned to more than one property, and EF Core supports bulk updates against complex type properties, which owned types historically did not; keep persistence entirely out of the domain by defining all fluent mapping, including HasField/UsePropertyAccessMode for private setters and backing fields, inside the infrastructure project's OnModelCreating, never inside the domain types themselves.
[ComplexType]
public sealed record Address(string Line1, string? Line2, string City, string PostalCode);
// In the infrastructure project, not the domain project:
modelBuilder.Entity<Customer>().ComplexProperty(c => c.BillingAddress);Because a complex type has no identity of its own and cannot be queried or tracked independently, it maps cleanly onto DDD's definition of a value object, compared by its values rather than by identity — the same Address record can be shared as both a billing and shipping address on a Customer without EF Core treating them as one shared, trackable row. For entities and aggregates, keeping persistence out of the domain project means avoiding data annotations tied to EF Core specifically, using private constructors with a parameterless protected or private constructor EF Core can use for materialization, and configuring backing fields for collections through fluent configuration in infrastructure rather than through public setters the domain would otherwise need to expose. The domain project ends up with no package reference to Microsoft.EntityFrameworkCore at all — the infrastructure project depends on domain types, never the reverse.
What interviewers look for: knowing EF Core complex types as the current tool for value objects, understanding the value-versus-reference-semantics distinction from owned types, and a clear answer on keeping all EF Core-specific configuration in infrastructure.
Q10 A team insists every microservice must be its own bounded context, one to one. Is that guidance correct?#
Short answer: It's a reasonable default, not a law: a bounded context can, in principle, contain multiple aggregates and even be implemented as more than one deployable service if those services stay under one team's ownership and one coherent model, and conversely a single deployment unit, such as a modular monolith, can legitimately host more than one bounded context as long as each context's internal model stays isolated from the others' inside the codebase — the actual danger isn't the ratio, it's a service boundary that cuts through the middle of a single bounded context or aggregate.
The failure mode to watch for is splitting a bounded context in a way that separates two things that need transactional or same-model consistency across a network call — for example, putting Order in one service and OrderLine in another when they're really one aggregate, forcing a distributed transaction or fragile choreographed consistency to enforce an invariant that should have been a single in-process operation. One bounded context implemented as two services is defensible when there's a genuine independent scaling or deployment reason and the team has deliberately designed the split around a real sub-boundary within the context, such as separating a command-handling service from a read-model projector under CQRS. Two bounded contexts sharing one deployable, as in a well-structured modular monolith, is equally defensible early in a system's life, when the operational cost of running many services outweighs the isolation benefit, provided the module boundaries inside the monolith are enforced as strictly as they would be across a network. The one-to-one default is a good starting heuristic precisely because it's hard to accidentally cut through an aggregate if the service and the context are the same thing — but it's a heuristic, not the actual definition of correctness.
What interviewers look for: identifying the real risk — a service boundary cutting through an aggregate or a bounded context — rather than treating the one-to-one ratio itself as the rule being tested.
Quick-Fire Round#
| Question | Answer |
|---|---|
| What defines a bounded context's boundary? | Where the ubiquitous language and domain model stay consistent; outside it, the same word can mean something else. |
| What should drive aggregate size? | Which invariants must be enforced atomically in a single transaction — nothing more. |
| How do aggregates reference each other? | By identity (an ID), never by direct object reference. |
| What's the core difference between a domain and integration event? | Domain events stay in-process; integration events cross a boundary and need a stable, versioned contract. |
| What problem does the Outbox pattern solve? | The dual-write problem — a DB commit and a message publish can't be one atomic operation without it. |
| What guarantee does the Outbox pattern give, and what does that require of consumers? | At-least-once delivery, which requires idempotent consumers. |
| When do you skip an Anti-Corruption Layer? | Integrating with another well-designed bounded context that already publishes a stable contract. |
| What EF Core feature (since EF Core 8) is recommended for value objects? | Complex types — value semantics, shareable instances, and support for bulk updates. |
How to Prepare#
- Practice identifying a bounded context boundary from linguistic divergence in a shared term, with a concrete example ready.
- Be able to size an aggregate by naming the specific invariant that must be atomic, not just "aggregates should be small."
- Rehearse the domain-event-versus-integration-event distinction and the coupling risk of conflating them, since it's one of the most commonly tested DDD questions.
- Know the Outbox pattern well enough to sketch it in code, including why consumers must be idempotent.
- Have a clear, criteria-based answer for when to build an Anti-Corruption Layer versus when it's unnecessary ceremony.
- Know EF Core complex types by name as the current recommended tool for value objects, and how they differ from owned entity types.