Bounded Contexts
In PearDrop, modules represent bounded contexts - distinct areas of your business domain with clear boundaries and their own domain models.
What is a Bounded Context?
A bounded context is a boundary within which a domain model is defined and applicable. Key characteristics:
- Explicit boundaries: Clear separation between contexts
- Distinct domain models: Each context has its own vocabulary and models
- Isolated data: Separate database schemas or tables
- Autonomous: Can be developed, tested, and deployed independently
- Communication via integration: Contexts communicate through well-defined contracts (Integration Events)
In PearDrop, one module = one bounded context.
PearDrop Module Structure
Each module follows a standard structure that enforces bounded context boundaries:
modules/{module-name}/
├── source/
│ ├── PearDrop.{ModuleName}/ # Core module (server-side)
│ │ ├── Domain/ # Domain model
│ │ │ ├── {AggregateContext}/ # Aggregate boundary
│ │ │ │ ├── AggregateRoot/ # Root entity
│ │ │ │ ├── DomainEvents/ # Internal events
│ │ │ │ ├── IntegrationEvents/ # Cross-context events
│ │ │ │ ├── EventHandlers/ # Event handlers
│ │ │ │ └── Configuration/ # EF configuration
│ │ ├── Data/ # Database context
│ │ ├── Infrastructure/ # Implementation details
│ │ └── Queries/ # Query models
│ ├── PearDrop.{ModuleName}.Client/ # Client contracts
│ │ ├── Commands/ # Command DTOs
│ │ ├── Queries/ # Query DTOs
│ │ └── Contracts/ # Public interfaces
│ └── PearDrop.{ModuleName}.Radzen/ # UI components (optional)
└── tests/ # Unit/integration tests
Framework Modules (Bounded Contexts)
PearDrop provides several framework modules representing common bounded contexts:
Authentication Module
Domain: User identity, authentication, authorization
Aggregates:
User- User accounts with credentials and profilesRole- Roles with permissionsExternalUser- External identity provider mappings
Responsibilities:
- User registration and login
- Password management and history
- Role-based access control
- External authentication (Azure AD, OAuth)
Integration Events:
UserRegisteredIntegrationEvent- New user createdUserEnabledIntegrationEvent- User account activatedUserDisabledIntegrationEvent- User account deactivatedPasswordChangedIntegrationEvent- Password updated
Files Module
Domain: Document storage and management
Aggregates:
Directory- Root-level file directoriesSubdirectory- Nested folders within directoriesFile- Individual files with metadata
Responsibilities:
- File upload and download
- Directory hierarchy management
- File metadata and versioning
- Azure Blob Storage or local filesystem integration
Integration Events:
FilesDeletedIntegrationEvent- Files removed by user
Multitenancy Module
Domain: Tenant isolation and management
Aggregates:
Tenant- Tenant configuration and settings
Responsibilities:
- Tenant creation and configuration
- Tenant identification (host/URL-based)
- Tenant isolation in data access
Integration Events:
TenantCreatedIntegrationEvent- New tenant provisioned
Application-Specific Modules
Your application defines its own bounded contexts as modules:
Example: TaskFlow Module
Domain: Task and project management
TaskFlow.App.Module/
├── Domain/
│ ├── TaskAggregate/
│ │ ├── AggregateRoot/
│ │ │ └── Task.cs
│ │ ├── DomainEvents/
│ │ │ ├── TaskCreatedDomainEvent.cs
│ │ │ └── TaskStatusChangedDomainEvent.cs
│ │ ├── IntegrationEvents/
│ │ │ └── TaskCompletedIntegrationEvent.cs
│ │ └── EventHandlers/
│ │ └── TaskCreatedEventHandler.cs
│ └── ProjectAggregate/
│ └── AggregateRoot/
│ └── Project.cs
├── Data/
│ ├── TaskFlowContext.cs # Write model DbContext
│ ├── ReadModel/
│ │ └── TaskFlowReadDbContext.cs # Read model DbContext
│ └── TaskFlowReadModels.cs # IModuleReadModels implementation
└── Queries/
└── Projections/
├── TaskProjection.cs # Read-only task model
└── ProjectProjection.cs # Read-only project model
Aggregates:
Task- Individual work items with status, assignmentsProject- Collections of related tasks
Integration Events:
TaskCompletedIntegrationEvent- Notify other systems when tasks finishProjectArchivedIntegrationEvent- Project moved to archived state
Context Boundaries and Communication
Within a Bounded Context (Domain Events)
Use domain events for coordination within the same bounded context:
// Location: TaskFlow.App.Module/Domain/TaskAggregate/AggregateRoot/Task.cs
public class Task : Entity, IAggregateRoot
{
public ResultWithError<BluQubeErrorData> Complete(DateTime completedAt)
{
if (this.Status == TaskStatus.Completed)
{
return ResultWithError.Fail<BluQubeErrorData>(
new BluQubeErrorData(ErrorCodes.CoreValidation, "Task is already completed"));
}
this.Status = TaskStatus.Completed;
this.CompletedAt = completedAt;
// Raise domain event - same context
this.AddDomainEvent(new TaskCompletedDomainEvent(
this.Id,
this.ProjectId,
completedAt));
return ResultWithError.Ok<BluQubeErrorData>();
}
}
// Location: TaskFlow.App.Module/Domain/ProjectAggregate/EventHandlers/
public class TaskCompletedEventHandler : INotificationHandler<TaskCompletedDomainEvent>
{
public async Task Handle(TaskCompletedDomainEvent evt, CancellationToken ct)
{
// Update Project aggregate in SAME TRANSACTION
var project = await projectRepo.FindOne(...);
project.Value.UpdateTaskCount();
// SaveChanges called by command handler
}
}
Domain events characteristics:
- Execute before SaveChanges (same transaction)
- Can block/rollback the operation
- Used for maintaining invariants within context
- MediatR dispatches events synchronously
Across Bounded Contexts (Integration Events)
Use integration events for cross-context communication:
// Command handler publishes integration event
if (result.IsSuccess)
{
await integrationEventPublisher.PublishAsync(
new TaskCompletedIntegrationEvent(
task.Id,
task.Title,
timeProvider.GetUtcNow()),
cancellationToken);
}
// Subscriber in DIFFERENT module (e.g., Notifications module)
public class TaskCompletedSubscriber : ICapSubscribe
{
[CapSubscribe(TaskflowCapTopics.TaskCompleted)]
public async Task Handle(
TaskCompletedIntegrationEvent @event,
CancellationToken cancellationToken)
{
// Send email notification (separate transaction)
await emailService.SendTaskCompletedEmailAsync(@event, cancellationToken);
}
}
Integration events characteristics:
- Published after SaveChanges succeeds
- CAP library provides durable delivery
- Cannot block/rollback the originating operation
- Eventual consistency across contexts
- Subscribers process asynchronously with automatic retries
Extracting New Bounded Contexts
As your application grows, you may identify new bounded contexts to extract:
Signs You Need Context Extraction
- Different rates of change: Parts of your domain evolve independently
- Different teams: Separate teams own different areas
- Scalability needs: Parts of the system have different scaling requirements
- Distinct domain languages: Different vocabularies for different features
- Low coupling: Few dependencies between areas
Example: Equipment Management → Separate Module
Starting state (monolithic):
MyApp.Module/
├── Domain/
│ ├── TaskAggregate/
│ ├── ProjectAggregate/
│ └── EquipmentAggregate/ # Mixed with tasks/projects
Extracted state:
MyApp.TaskManagement/ # Task/Project context
├── Domain/
│ ├── TaskAggregate/
│ └── ProjectAggregate/
MyApp.EquipmentManagement/ # Equipment context
├── Domain/
│ └── EquipmentAggregate/
PearDrop CLI support:
# Extract Equipment context to new module
peardrop extract-context Equipment \
--from-module MyApp.Module \
--to-module MyApp.EquipmentManagement
See Bounded Context Extraction Guide for detailed extraction workflows.
Best Practices
✅ DO
- One module per bounded context: Clear organizational boundaries
- Use integration events for cross-context communication: Avoid direct database access
- Define explicit contracts in
.Clientprojects: Public API for the context - Separate read/write models per context: Independent DbContexts
- Version integration events carefully: Breaking changes affect multiple contexts
❌ DON'T
- Share aggregates across contexts: Each context owns its aggregates
- Query other contexts' databases directly: Use integration events or dedicated query APIs
- Create circular dependencies between contexts: Use integration events to break cycles
- Mix business domain concerns: Keep contexts focused on a single area
- Leak domain entities in public APIs: Use DTOs in commands/queries
Module Registration Pattern
Each module registers its services via extension methods:
// Server-side registration (Program.cs)
builder.Services.AddPearDropAuthentication(builder.Configuration);
builder.Services.AddPearDropFiles(builder.Configuration);
builder.Services.AddTaskFlowModule(builder.Configuration);
// Client-side registration (Blazor WebAssembly)
builder.Services.TryAddPearDropClient(builder.HostEnvironment.BaseAddress);
builder.Services.AddPearDropAuthentication(); // Auth client
builder.Services.AddTaskFlowModule(); // App client
Module independence:
- Each module configures its own DbContexts
- Each module registers its own read models
- Each module handles its own migrations
- Each module can be deployed as separate NuGet package
Testing Bounded Contexts
Test each bounded context independently:
// Integration test for TaskFlow context
public class TaskCommandTests : IClassFixture<TaskFlowContextFixture>
{
[Fact]
public async Task CreateTask_ShouldSucceed()
{
// Arrange - only TaskFlow dependencies
var command = new CreateTaskCommand("Test Task", projectId);
// Act - within TaskFlow context
var result = await commandRunner.RunAsync(command);
// Assert - TaskFlow invariants
Assert.True(result.IsSuccessful);
var task = await readModels.Tasks.FirstOrDefaultAsync(t => t.Id == result.Data!.TaskId);
Assert.NotNull(task);
}
}
Summary
✅ Modules = Bounded Contexts: One-to-one mapping in PearDrop
✅ Clear Boundaries: Each context owns domain model, database schema, business logic
✅ Domain Events: Strong consistency within context (before SaveChanges)
✅ Integration Events: Eventual consistency across contexts (after SaveChanges)
✅ Independent Evolution: Contexts can change independently
✅ CLI Support: Extract contexts as applications grow
Bounded contexts in PearDrop provide clear organizational boundaries that enable teams to work independently while maintaining system coherence through well-defined integration points.
Further Reading
- Domain-Driven Design - DDD overview and strategic patterns
- Aggregates - Entity base class and aggregate design
- Domain Events - Within-context coordination
- Integration Events - Cross-context communication
- Read Models - Query optimization per context