
Clean Architecture and Domain-Driven Design are often mentioned in the same breath. Robert C. Martin's well-known ring diagram, however, shows only one domain with a single model at its center. As soon as a system has several subdomains and Bounded Contexts, this picture is no longer enough. This article shows how the rings become a garlic bulb, what that means for the Dependency Rule, and how contexts communicate cleanly across their boundaries.
Hardly any architecture diagram hangs in as many team rooms as Uncle Bob's. It has shown a whole generation of .NET developers that the database is a detail and the business rules belong in the center.
And yet teams keep stumbling over the same spot. The solution has the four projects Domain, Application, Infrastructure and Api, the dependencies point dutifully inward, and still, after two years, a model has grown that nobody understands anymore. The Customer class has eighty properties, and every change requires coordination between three departments. The cause is not the rings, but what the diagram leaves unsaid: it knows exactly one center.
This article turns the onion into a bulb and then turns to what causes the most trouble in practice: communication across the boundaries between contexts. The example is the contact management system from our seminar, with the contexts Contacts, Newsletter and Sales.
Robert C. Martin published “The Clean Architecture” on his blog in August 2012; the book followed in 2017. He himself says that the idea is not new: Hexagonal Architecture, Onion Architecture and related approaches pursue the same goal. The business logic is supposed to be independent of framework, user interface, database and external services.

Figure 1 is read from the inside out. At the core are the Entities with the business rules that would apply even without software, such as Contact and EmailAddress. Around them are the Use Cases, in .NET usually implemented as commands and queries. The Interface Adapters translate between the use cases and the outside world: repository implementations, endpoints, DTOs. On the outside are the Frameworks and Drivers such as ASP.NET Core, EF Core and the database.
A single rule holds the diagram together, the Dependency Rule: source code dependencies point only inward. Nothing in an inner ring knows anything about an outer one. If a use case needs the database, it defines an interface such as IContactRepository on the inside, and on the outside a class using EF Core implements it. Martin stresses that the number of rings is schematic. What matters is the direction.
What Martin does not state explicitly, but what every viewer reads into it: there is exactly one core. The diagram has one center, and in this center lies one model. For a manageable application, that holds true. For a company with sales, marketing and accounting, it does not, and this is exactly where Domain-Driven Design comes in.
In the contact management system, the same person appears in three places. For master data maintenance, they are a contact with name, email address, postal address and status. For newsletter marketing, they are a recipient with email address and consent; nobody there cares about the postal address. For sales, they are a point of contact with company, role and open sales opportunities. Three terms, three records, three sets of rules.
The classic reflex is to merge everything into one class. Everyone knows the result: a model that belongs to everyone and that nobody dares to change anymore. DDD proposes the opposite approach and distinguishes two terms that are often confused.
Table 1: Subdomain and Bounded Context
A subdomain exists even without software. A distinction is made between the Core Domain, which sets a company apart from the competition, Supporting Subdomains, which are necessary but make no difference in the market, and Generic Subdomains, which are bought in. In our example, Sales is the core, Contacts and Newsletter are supporting, and email delivery is generic.
A Bounded Context, by contrast, is a design decision. It is the boundary within which a model applies unambiguously, with its own language, its own rules and usually its own data. Ideally, a Bounded Context corresponds to exactly one subdomain. Across the boundaries, only the identity is preserved: contact, recipient and point of contact share the same ContactId, but no model.
Anyone with three models cannot put them all into the same core without ending up with the class with eighty properties again. The solution is simpler than it sounds: keep the ring diagram, but draw it once per Bounded Context. Push these onions together, and a garlic bulb emerges.

In Figure 2, each clove is a Bounded Context. Each has its own domain with its own aggregates: Contact in the Contacts context, DistributionList in Newsletter, Opportunity in Sales. Each has its own use cases, its own infrastructure and its own presentation layer. The ring names follow the project structure that is common in .NET solutions: Domain, Application, Infrastructure and Presentation. They correspond to Martin's Entities, Use Cases, Interface Adapters and Frameworks.
The Dependency Rule applies unchanged, but only within a clove. In every clove the dependencies point inward, indicated in Figure 2 by one arrow per context. There is also a second rule, which the classic diagram does not need because it has only one core: no context reaches into the rings of another. The Newsletter context does not reference Contacts.Domain, Sales does not read from the Contacts table, and no handler in Sales directly calls a command from the Contacts context.
In short: inside, dependencies point to the clove's own domain; across, there are none. Whatever still has to cross the boundary runs through the seams.
At the center of the bulb sits the Shared Kernel, a small, jointly maintained part of the model. Typical contents are strongly typed IDs such as ContactId, a few general value objects, and the base types for entities and domain events. That it sits in the center fits the Dependency Rule: all cloves may depend on it, while it knows none of them.
At the same time, it is the most delicate part of the bulb, because every change affects all contexts. The rule of thumb: whatever goes in must have the same meaning everywhere. EmailAddress passes this test, Customer never does.
Because every clove stands on its own, each can have its own depth. The Core Domain Sales gets a rich model, CQRS and plenty of tests. In the supporting context Newsletter, simple CRUD behind a clean interface is often enough. Generic subdomains such as email delivery are bought in and appear only as adapters in the Infrastructure ring. DDD is thus not a decision for the whole system, but for each clove individually.
Bounded Contexts are never completely independent. The Newsletter context needs to learn about new contacts, and Sales needs the names and companies of its points of contact. These dependencies should be designed deliberately instead of emerging through shared tables. The bulb provides a fixed place for this: the seam.
Every relationship between two contexts has a direction. The upstream context supplies data or a model, the downstream context uses it. If the upstream changes its model, the downstream feels it, not the other way around. Eric Evans names the patterns for this in the DDD Reference: Partnership, Shared Kernel, Customer/Supplier, Conformist, Anticorruption Layer, Open Host Service and Separate Ways. Together they make up the Context Map, and in Figure 2 it sits where it belongs: in the seams.
Two questions help with the choice: how much influence do you have on the upstream, and how well does its model fit yours? In Figure 2, Contacts offers its data as an Open Host Service and publishes Integration Events, which Newsletter receives as a customer in the sense of Customer/Supplier. Sales has its own, older model and protects it with an Anticorruption Layer. Between Newsletter and Sales there is deliberately no integration: Separate Ways.
The bulb makes visible a rule that often goes unnoticed: communication between contexts begins and ends in the outer rings. No domain talks directly to another. An event does originate in the domain, but it is translated into an Integration Event in the Application or Infrastructure ring. On the other side, the Infrastructure receives it and calls a use case of its own. This keeps every core free of foreign models, and the Dependency Rule also holds across the seam.
For asynchronous communication, a distinction is worthwhile that Microsoft's guide to .NET microservices also makes:
Table 2: Domain Events stay in the clove, Integration Events cross the seam
Integration Events are thus a contract with other teams. That is why translation happens at the boundary: a handler in the Contacts context reacts to the Domain Event ContactCreated and publishes an Integration Event from it, with its own versioned format.
// Contacts.Contracts: the Published Language of the Contacts context
public sealed record ContactRegisteredV1(Guid ContactId, string Email, DateTime OccurredOnUtc);// Contacts.Application: translates the Domain Event into the contract
public sealed class PublishContactRegistered(IIntegrationEventPublisher publisher)
: INotificationHandler<DomainEventNotification<ContactCreated>>
{
public Task Handle(DomainEventNotification<ContactCreated> notification, CancellationToken ct)
{
var e = notification.DomainEvent;
return publisher.PublishAsync(
new ContactRegisteredV1(e.ContactId.Value, e.Email.Value, e.OccurredOnUtc), ct);
}
}The contract lives in a small project of its own, Contacts.Contracts. Only this project may be referenced by Newsletter, never Contacts.Domain. This keeps the domain model free to change. If the contract changes, ContactRegisteredV2 is created alongside the first version.
As soon as events leave the process, the use case writes to two systems without a shared transaction: the database and the message broker. If the process crashes in between, the contact is saved, but Newsletter never learns about it. If a revoked consent is lost this way, a technical error becomes a data protection issue.
The solution is called Transactional Outbox. The Integration Event is not sent directly but stored as a row in a table OutboxMessages, in the same transaction as the business change. A background service reads the unsent rows, publishes them via the broker and marks them as processed. Because messages may arrive more than once, receivers must be idempotent: a message delivered twice must not create a second recipient. Libraries such as MassTransit, NServiceBus or Wolverine come with outbox and inbox built in; check the current license terms before using them.
Not everything can be solved with events. If Sales needs the current name of a point of contact when creating an offer, it asks the Contacts context. To keep the foreign model from seeping into its own clove, Sales describes in its own language what it needs. A translator in its own Infrastructure ring does the rest:
// Sales.Application: Sales states in its own language what it needs
public interface IContactPersonDirectory
{
Task<ContactPerson?> FindAsync(ContactId id, CancellationToken ct);
}// Sales.Infrastructure: the Anticorruption Layer translates the foreign model
public sealed class ContactsApiTranslator(ContactsApiClient client) : IContactPersonDirectory
{
public async Task<ContactPerson?> FindAsync(ContactId id, CancellationToken ct)
{
var dto = await client.GetContactAsync(id.Value, ct);
return dto is null ? null : new ContactPerson(id, dto.DisplayName, dto.Company);
}
}Sales knows only ContactPerson and IContactPersonDirectory. If the Contacts context changes its API, exactly one class needs to be adapted. This is the same dependency inversion that keeps the database at a distance within a clove, only applied to a neighboring context.
In Solution Explorer, the cut is made by Bounded Context first and only then by layers. Each clove gets a folder with its projects, for example Contacts.Domain, Contacts.Application, Contacts.Infrastructure, Contacts.Api and Contacts.Contracts. Alongside are Newsletter, Sales, a small SharedKernel and a Host as the composition root.
Within each clove, the compiler enforces the direction through project references, while an architecture test guards the cross rule, for example with NetArchTest. For getting started, we recommend a modular monolith: all cloves in one solution, one deployment, clean boundaries. If a context later needs to scale independently, it can be extracted as a microservice, because the seams are already right.
Uncle Bob's ring diagram is not wrong. It describes one clove. Anyone who takes DDD seriously discovers several subdomains, designs several Bounded Contexts for them and thus ends up with several ring systems. Together they form a bulb, and four guiding principles apply to it:
The most important step is not a technical one: the seams can only be found together with the domain experts, for example in an Event Storming session. How to continue step by step from there, all the way to the outbox, is shown in our four-day seminar “Clean Architecture und DDD mit C#”.
Robert C. Martin: The Clean Architecture. Blog, 13 August 2012. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Eric Evans: Domain-Driven Design Reference. https://www.domainlanguage.com/ddd/reference/
Vaughn Vernon: Implementing Domain-Driven Design. Addison-Wesley, 2013
Martin Fowler: Bounded Context. https://martinfowler.com/bliki/BoundedContext.html
Microsoft: .NET Microservices: Architecture for Containerized .NET Applications. https://learn.microsoft.com/dotnet/architecture/microservices/
Chris Richardson: Pattern: Transactional Outbox. https://microservices.io/patterns/data/transactional-outbox.html

Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.
Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.