INTRODUCTION TO THE TUTORIAL
Welcome to this guide on designing application architectures that combine the power of Domain-Driven Design with Capability-Centric Architecture. This tutorial is specifically crafted for software engineers who want to master a unified approach that works equally well for embedded systems, enterprise applications, and everything in between.
Before we dive into the step-by-step process, let us understand why combining these two approaches creates such a powerful architectural foundation. Domain-Driven Design provides us with both strategic patterns (bounded contexts, context maps, context relationships) and tactical patterns (entities, value objects, aggregates, repositories, factories, domain services) to understand and model complex business domains. Capability-Centric Architecture gives us the structural patterns to implement these models in a way that remains maintainable, testable, and evolvable over time.
The beauty of this combination lies in how naturally they complement each other. DDD helps us identify what our system should do by focusing on the domain and its bounded contexts. CCA helps us determine how to structure our system by organizing functionality into capabilities with clear separation between domain logic, infrastructure concerns, and external interfaces. Every DDD concept has a natural home within the CCA structure, creating a seamless integration.
COMPLETE CONCEPT MAPPING: DDD TO CCA
Before we begin the step-by-step process, we need to establish a complete understanding of how every DDD concept maps to CCA structures. This mapping serves as our reference throughout the tutorial.
STRATEGIC DDD PATTERNS AND CCA MAPPING:
Bounded Context (DDD) maps to Capability (CCA)
- A bounded context defines a linguistic and conceptual boundary
- A capability encapsulates that boundary with its nucleus structure
- The ubiquitous language of the context becomes the vocabulary of the capability's Essence layer
Context Map (DDD) maps to Capability Dependency Graph (CCA)
- Context maps show relationships between bounded contexts
- The capability registry manages these relationships as contract bindings
- Each line on the context map becomes a contract between capabilities
Customer-Supplier Relationship (DDD) maps to Contract Provision-Requirement (CCA)
- The supplier context provides a contract
- The customer context requires that contract
- The capability registry enforces this relationship
Shared Kernel (DDD) maps to Shared Contract Library (CCA)
- Common domain concepts shared between contexts
- Implemented as shared contract interfaces and value objects
- Both capabilities depend on the same contract definitions
Anticorruption Layer (DDD) maps to Adaptation Layer with Translation (CCA)
- Protects the domain model from external models
- Implemented in the Adaptation layer of the capability
- Translates between internal domain model and external representations
Published Language (DDD) maps to Event-Based Contract (CCA)
- A well-documented, stable integration contract
- Implemented as event schemas in the contract
- Capabilities publish events that others subscribe to
Open Host Service (DDD) maps to Public Contract Interface (CCA)
- A service that provides access to a bounded context
- Implemented as the Provision part of a capability contract
- Designed for consumption by multiple other capabilities
Conformist (DDD) maps to Direct Contract Consumption (CCA)
- Downstream context conforms to upstream model
- Capability requires and uses contract without translation
- No anticorruption layer needed
Partnership (DDD) maps to Bidirectional Contract Binding (CCA)
- Two contexts cooperate and evolve together
- Both capabilities provide and require each other's contracts
- Coordinated evolution through shared evolution envelopes
TACTICAL DDD PATTERNS AND CCA MAPPING:
Entity (DDD) maps to Essence Layer Class with Identity (CCA)
- Objects with identity and lifecycle
- Implemented in the Essence layer
- Identity preserved across persistence boundaries
Value Object (DDD) maps to Essence Layer Immutable Class (CCA)
- Objects defined by attributes, not identity
- Implemented in the Essence layer as immutable classes
- Shared across entity boundaries
Aggregate (DDD) maps to Essence Layer Aggregate Root (CCA)
- Cluster of entities and value objects with consistency boundary
- Aggregate root implemented in Essence layer
- Enforces invariants across the entire aggregate
Repository (DDD) maps to Realization Layer Persistence (CCA)
- Provides collection-like interface to aggregates
- Implemented in the Realization layer
- Translates between domain objects and database
Factory (DDD) maps to Essence or Realization Layer Factory (CCA)
- Creates complex aggregates or entities
- Simple factories in Essence layer (pure construction logic)
- Complex factories in Realization layer (if infrastructure needed)
Domain Service (DDD) maps to Essence Layer Service (CCA)
- Domain logic that doesn't belong to entities
- Implemented in the Essence layer as stateless services
- Operates on domain objects passed as parameters
Application Service (DDD) maps to Realization Layer Orchestration (CCA)
- Coordinates use cases and transactions
- Implemented in the Realization layer
- Orchestrates Essence domain logic with infrastructure
Domain Event (DDD) maps to Contract Event (CCA)
- Represents something that happened in the domain
- Defined in the contract as event schemas
- Published through the Adaptation layer
Specification (DDD) maps to Essence Layer Predicate (CCA)
- Encapsulates business rules for selection/validation
- Implemented in the Essence layer
- Used by repositories and domain services
Module (DDD) maps to Capability Internal Package Structure (CCA)
- Organizes related domain concepts
- Implemented as packages within a capability
- Essence, Realization, Adaptation are top-level modules
CORE CONCEPTS OVERVIEW
From Domain-Driven Design, we draw upon several key concepts organized into strategic and tactical patterns. Strategic patterns help us understand the big picture: Bounded Contexts represent clear boundaries within which a particular domain model is defined and applicable. Context Maps show how these contexts relate to each other through patterns like Customer-Supplier, Shared Kernel, Anticorruption Layer, Published Language, Open Host Service, Conformist, and Partnership.
Tactical patterns help us implement the domain model: Entities have identity and lifecycle. Value Objects are defined by their attributes. Aggregates cluster related entities and value objects with consistency boundaries. Repositories provide collection-like access to aggregates. Factories create complex domain objects. Domain Services encapsulate domain logic that doesn't naturally fit within entities. Application Services coordinate use cases. Domain Events represent things that happened in the domain.
From Capability-Centric Architecture, we adopt a complementary set of concepts. A Capability represents a cohesive unit of functionality that delivers value. Each capability is structured as a Capability Nucleus containing three distinct layers. The Essence layer holds pure domain logic with no external dependencies. The Realization layer contains the technical mechanisms needed to make the essence work in the real world, including database access, hardware integration, or message queue connections. The Adaptation layer provides interfaces for interaction with other capabilities and external systems.
Capabilities interact through Contracts rather than direct dependencies. A contract specifies what a capability provides through its Provision, what it requires through its Requirements, and the interaction patterns through its Protocol. This contract-based interaction enables independent evolution and prevents circular dependencies.
The synthesis of DDD and CCA occurs when we recognize that a Bounded Context from DDD naturally maps to one or more Capabilities in CCA. The domain model within a bounded context becomes the Essence of a capability. The repositories and infrastructure concerns become the Realization. The context mapping between bounded contexts becomes the Contracts between capabilities. Every tactical DDD pattern has a specific place within the capability structure.
STEP ONE: UNDERSTANDING THE PROBLEM DOMAIN AND IDENTIFYING BOUNDED CONTEXTS
The first step in designing any application architecture is developing a deep understanding of the problem domain and identifying the bounded contexts within it. This step forms the foundation upon which everything else is built, and rushing through it inevitably leads to architectural problems later.
Rationale for this step: Without a thorough understanding of the domain and its natural boundaries, we cannot identify the right capabilities, design appropriate contracts, or separate essential domain logic from technical implementation details. Domain-Driven Design emphasizes that the software should reflect the mental model of domain experts, and this can only happen if we first understand that mental model and recognize where different models apply.
How to execute this step: Begin by engaging with domain experts through structured conversations. These are not requirements-gathering sessions in the traditional sense, but rather collaborative explorations of how the domain works. Ask domain experts to describe their work in their own language, paying careful attention to the terms they use, the distinctions they make, and the relationships they describe.
Create a glossary of domain terms as you go, noting when the same word means different things in different contexts or when different words refer to the same concept. These linguistic boundaries often indicate bounded context boundaries. Look for natural clusters of related concepts that domain experts discuss together. These clusters often become bounded contexts and, subsequently, capabilities.
Pay special attention to the following signals that indicate bounded context boundaries:
- Different meanings for the same term (e.g., "Customer" means different things in Sales vs Support)
- Different teams or departments working on different aspects
- Different rates of change (some areas change frequently, others are stable)
- Different data ownership and lifecycle management
- Natural seams where communication becomes more formal
Consider a concrete example to illustrate this step. Imagine we are building a comprehensive system for an industrial manufacturing plant. Through conversations with plant operators, maintenance engineers, production managers, quality assurance specialists, and supply chain coordinators, we discover several distinct areas of concern.
The operators talk about controlling individual machines, monitoring sensor readings in real-time, responding to alarms, and adjusting setpoints. Their language is precise and technical, focused on immediate operational concerns. When they say "status," they mean the current operational state of a machine right now.
The maintenance engineers discuss equipment health, predictive maintenance schedules, spare parts inventory, maintenance history, and failure patterns. They use some of the same terms as operators but with different meanings. When maintenance engineers say "status," they mean the health condition based on historical trends and predictive analytics, not the immediate operational state.
The production managers focus on production targets, throughput optimization, quality metrics, overall equipment effectiveness (OEE), and production schedules. They think in terms of batches, production runs, and efficiency metrics. Their "status" means whether production is on track to meet targets.
The quality assurance specialists talk about quality control points, defect rates, statistical process control, compliance requirements, and quality certifications. They have their own vocabulary around quality metrics and testing procedures.
The supply chain coordinators discuss raw material inventory, supplier relationships, delivery schedules, and material requirements planning. They think about the flow of materials through the plant.
Each of these represents a different bounded context with its own ubiquitous language, its own model of the domain, and its own concerns. The linguistic differences signal that we are dealing with distinct bounded contexts that will become distinct capabilities.
Content created in this step: We produce several key artifacts:
Domain Model Sketch: A conceptual map showing key concepts, their relationships, and their boundaries. This is not a detailed UML diagram but rather a visual representation of how domain experts think about their work. For each area of concern, we sketch the main concepts and how they relate.
Ubiquitous Language Glossary: A comprehensive glossary organized by bounded context. For each context, we define the terms as they are used within that context. We explicitly note where the same term has different meanings in different contexts.
Bounded Context Catalog: A list of identified bounded contexts with descriptions:
Bounded Context: Machine Control
- Purpose: Real-time control of individual machines
- Key Concepts: Machine, Sensor, Actuator, ControlLoop, Setpoint, Alarm
- Responsibilities: Read sensors, execute control algorithms, drive actuators, raise alarms
- Language Characteristics: Precise, technical, real-time focused
Bounded Context: Equipment Health Monitoring
- Purpose: Track equipment condition and predict maintenance needs
- Key Concepts: Equipment, HealthMetric, MaintenanceSchedule, Prediction, FailurePattern
- Responsibilities: Collect health data, analyze trends, predict failures, schedule maintenance
- Language Characteristics: Historical, analytical, predictive
Bounded Context: Production Planning
- Purpose: Plan and optimize production schedules
- Key Concepts: ProductionOrder, Batch, Schedule, Resource, Capacity
- Responsibilities: Create schedules, allocate resources, track production progress
- Language Characteristics: Planning-oriented, optimization-focused
Bounded Context: Quality Assurance
- Purpose: Ensure product quality and compliance
- Key Concepts: QualityCheck, Defect, Specification, Compliance, Certificate
- Responsibilities: Define quality standards, perform inspections, track defects
- Language Characteristics: Standards-based, compliance-focused
Bounded Context: Material Management
- Purpose: Manage raw materials and supplies
- Key Concepts: Material, Inventory, Supplier, Order, Delivery
- Responsibilities: Track inventory, order materials, manage suppliers
- Language Characteristics: Logistics-focused, supply-chain oriented
Context Boundary Justifications: For each identified boundary, we document why we believe it represents a true bounded context boundary, including linguistic evidence, organizational structure, and domain expert feedback.
For our manufacturing example, we have identified five bounded contexts. Each will become a capability in our CCA architecture. The boundaries are clear because each context has its own vocabulary, its own model, and its own set of concerns that are relatively independent of the others.
STEP TWO: CREATING THE CONTEXT MAP AND IDENTIFYING RELATIONSHIP PATTERNS
Once we have identified the bounded contexts, we need to map the relationships between them and identify which DDD context mapping patterns apply to each relationship. This step bridges the gap between domain understanding and architectural structure.
Rationale for this step: Bounded contexts do not exist in isolation; they must interact to deliver business value. The nature of these interactions profoundly affects how we design the contracts between capabilities. By making the relationships explicit and identifying the appropriate patterns, we can design contracts that properly reflect the domain relationships and avoid common integration pitfalls.
How to execute this step: For each pair of bounded contexts that need to interact, identify the nature of their relationship. Ask questions like: Which context is upstream (provides information) and which is downstream (consumes information)? Is the relationship symmetric or asymmetric? Does one context have more power or influence? How much do the contexts need to coordinate their evolution?
DDD provides several context mapping patterns that describe common relationship types:
Customer-Supplier: The upstream context (supplier) provides a service that the downstream context (customer) consumes. The supplier has some obligation to support the customer, but the supplier's model takes precedence. The customer influences the supplier's roadmap but doesn't control it.
Shared Kernel: Two contexts share a subset of their domain model. This creates a strong dependency and requires close coordination, but it can reduce duplication for truly shared concepts. Changes to the shared kernel must be coordinated between both contexts.
Anticorruption Layer (ACL): The downstream context protects its domain model from the upstream context's model by creating a translation layer. This is essential when integrating with legacy systems or external systems whose models would corrupt your clean domain model.
Published Language: The upstream context defines a well-documented, stable integration language that downstream contexts can use. This is often implemented as a standardized event schema or API contract.
Open Host Service: The upstream context provides a service designed for consumption by multiple downstream contexts. The service is designed to be general-purpose rather than tailored to any specific consumer.
Conformist: The downstream context conforms to the upstream context's model without translation. This is appropriate when the upstream model is good enough and the cost of maintaining a translation layer isn't justified.
Partnership: Two contexts have a mutual dependency and must coordinate their evolution. Both teams work together to ensure compatibility. This pattern requires close collaboration and is typically used when two contexts are tightly coupled by business requirements.
Separate Ways: Two contexts have no relationship at all. They solve similar problems independently. This is actually a valid pattern when integration costs exceed the benefits.
Let us create a complete context map for our manufacturing system:
MANUFACTURING SYSTEM CONTEXT MAP
[Machine Control] --Customer/Supplier--> [Equipment Health Monitoring]
Pattern: Customer-Supplier with Published Language
Relationship: Machine Control is Supplier, Equipment Health is Customer
Nature: Machine Control publishes sensor data and machine events
Equipment Health subscribes to these events for analysis
Contract: MachineMonitoringContract with event-based protocol
[Production Planning] --Customer/Supplier--> [Machine Control]
Pattern: Customer-Supplier
Relationship: Production Planning is Customer, Machine Control is Supplier
Nature: Production Planning needs to know machine availability and status
Contract: MachineStatusContract with synchronous queries
[Production Planning] --Customer/Supplier--> [Equipment Health Monitoring]
Pattern: Customer-Supplier
Relationship: Production Planning is Customer, Equipment Health is Supplier
Nature: Production Planning needs maintenance schedules to plan around downtime
Contract: MaintenanceScheduleContract
[Quality Assurance] --Customer/Supplier--> [Production Planning]
Pattern: Customer-Supplier
Relationship: Quality Assurance is Customer, Production Planning is Supplier
Nature: Quality checks are tied to production batches
Contract: ProductionBatchContract
[Quality Assurance] --Customer/Supplier--> [Machine Control]
Pattern: Customer-Supplier with Published Language
Relationship: Quality Assurance is Customer, Machine Control is Supplier
Nature: Quality metrics are derived from machine sensor data
Contract: QualityDataContract with event-based protocol
[Material Management] --Partnership--> [Production Planning]
Pattern: Partnership
Relationship: Bidirectional dependency
Nature: Material availability affects production schedules,
production schedules drive material orders
Both teams must coordinate closely
Contracts: MaterialAvailabilityContract and MaterialRequirementContract
[Machine Control] --Anticorruption Layer--> [Legacy SCADA System]
Pattern: Anticorruption Layer
Relationship: Machine Control protects itself from legacy system's model
Nature: Legacy SCADA has poor data model, Machine Control translates
Implementation: Translation layer in Machine Control's Adaptation layer
[Material Management] --Anticorruption Layer--> [External ERP System]
Pattern: Anticorruption Layer
Relationship: Material Management protects itself from external ERP model
Nature: External ERP uses different terminology and data structures
Implementation: Translation layer in Material Management's Adaptation layer
This context map shown above is the strategic blueprint for our entire system. Each relationship will become a contract binding in our CCA implementation. The patterns tell us how to design those contracts.
Now let us translate this context map into CCA structures. We will create a visual representation showing how the context map becomes the capability dependency graph:
CAPABILITY DEPENDENCY GRAPH (CCA Translation)
Capability: MachineControl
Provides: MachineMonitoringContract (Published Language pattern)
MachineStatusContract (Open Host Service pattern)
QualityDataContract (Published Language pattern)
Requires: LegacySCADAContract (with ACL in Adaptation layer)
Capability: EquipmentHealthMonitoring
Provides: MaintenanceScheduleContract
Requires: MachineMonitoringContract (Conformist pattern - uses events as-is)
Capability: ProductionPlanning
Provides: ProductionBatchContract
MaterialRequirementContract (Partnership pattern)
Requires: MachineStatusContract (Conformist pattern)
MaintenanceScheduleContract (Conformist pattern)
MaterialAvailabilityContract (Partnership pattern)
Capability: QualityAssurance
Provides: QualityReportContract
Requires: ProductionBatchContract (Conformist pattern)
QualityDataContract (Conformist pattern)
Capability: MaterialManagement
Provides: MaterialAvailabilityContract (Partnership pattern)
Requires: MaterialRequirementContract (Partnership pattern)
ExternalERPContract (with ACL in Adaptation layer)
Now let us implement this in code. We will create the capability registry configuration that establishes all these relationships:
public class ManufacturingSystemConfiguration {
public void configureSystem(CapabilityRegistry registry) {
// Register Machine Control capability
registry.registerCapability(
CapabilityDescriptor.builder()
.name("MachineControl")
.boundedContext("Machine Control")
.implementationClass(MachineControlCapability.class)
.provides(MachineMonitoringContract.class)
.provides(MachineStatusContract.class)
.provides(QualityDataContract.class)
.requires(LegacySCADAContract.class)
.relationshipPattern(
MachineMonitoringContract.class,
ContextRelationship.PUBLISHED_LANGUAGE
)
.relationshipPattern(
MachineStatusContract.class,
ContextRelationship.OPEN_HOST_SERVICE
)
.anticorruptionLayer(
LegacySCADAContract.class,
SCADAAnticorruptionLayer.class
)
.build()
);
// Register Equipment Health Monitoring capability
registry.registerCapability(
CapabilityDescriptor.builder()
.name("EquipmentHealthMonitoring")
.boundedContext("Equipment Health Monitoring")
.implementationClass(EquipmentHealthCapability.class)
.provides(MaintenanceScheduleContract.class)
.requires(MachineMonitoringContract.class)
.relationshipPattern(
MachineMonitoringContract.class,
ContextRelationship.CONFORMIST
)
.build()
);
// Register Production Planning capability
registry.registerCapability(
CapabilityDescriptor.builder()
.name("ProductionPlanning")
.boundedContext("Production Planning")
.implementationClass(ProductionPlanningCapability.class)
.provides(ProductionBatchContract.class)
.provides(MaterialRequirementContract.class)
.requires(MachineStatusContract.class)
.requires(MaintenanceScheduleContract.class)
.requires(MaterialAvailabilityContract.class)
.relationshipPattern(
MaterialRequirementContract.class,
ContextRelationship.PARTNERSHIP
)
.relationshipPattern(
MaterialAvailabilityContract.class,
ContextRelationship.PARTNERSHIP
)
.build()
);
// Register Quality Assurance capability
registry.registerCapability(
CapabilityDescriptor.builder()
.name("QualityAssurance")
.boundedContext("Quality Assurance")
.implementationClass(QualityAssuranceCapability.class)
.provides(QualityReportContract.class)
.requires(ProductionBatchContract.class)
.requires(QualityDataContract.class)
.build()
);
// Register Material Management capability
registry.registerCapability(
CapabilityDescriptor.builder()
.name("MaterialManagement")
.boundedContext("Material Management")
.implementationClass(MaterialManagementCapability.class)
.provides(MaterialAvailabilityContract.class)
.requires(MaterialRequirementContract.class)
.requires(ExternalERPContract.class)
.relationshipPattern(
MaterialAvailabilityContract.class,
ContextRelationship.PARTNERSHIP
)
.anticorruptionLayer(
ExternalERPContract.class,
ERPAnticorruptionLayer.class
)
.build()
);
// Establish contract bindings based on context map
establishBindings(registry);
// Validate no circular dependencies
registry.validateDependencyGraph();
}
private void establishBindings(CapabilityRegistry registry) {
// Equipment Health subscribes to Machine Control events
registry.bindContract(
"EquipmentHealthMonitoring", // Customer
"MachineControl", // Supplier
MachineMonitoringContract.class
);
// Production Planning depends on Machine Control status
registry.bindContract(
"ProductionPlanning",
"MachineControl",
MachineStatusContract.class
);
// Production Planning depends on Equipment Health schedules
registry.bindContract(
"ProductionPlanning",
"EquipmentHealthMonitoring",
MaintenanceScheduleContract.class
);
// Quality Assurance depends on Production Planning batches
registry.bindContract(
"QualityAssurance",
"ProductionPlanning",
ProductionBatchContract.class
);
// Quality Assurance depends on Machine Control quality data
registry.bindContract(
"QualityAssurance",
"MachineControl",
QualityDataContract.class
);
// Partnership: Production Planning and Material Management
registry.bindContract(
"ProductionPlanning",
"MaterialManagement",
MaterialAvailabilityContract.class
);
registry.bindContract(
"MaterialManagement",
"ProductionPlanning",
MaterialRequirementContract.class
);
}
}
Content created in this step:
Context Map Diagram: A visual representation showing all bounded contexts and their relationships, annotated with the DDD pattern that applies to each relationship.
Relationship Pattern Catalog: For each relationship, we document:
- The pattern being used (Customer-Supplier, Partnership, ACL, etc.)
- The rationale for choosing this pattern
- The implications for contract design
- The implementation approach
Capability Dependency Graph: The CCA translation of the context map, showing which capabilities provide and require which contracts.
Registry Configuration Code: The actual code that registers capabilities and establishes contract bindings, with annotations indicating which DDD pattern each binding implements.
Anticorruption Layer Specifications: For relationships using the ACL pattern, we document what translation is needed and where it will be implemented.
STEP THREE: DESIGNING CAPABILITY CONTRACTS BASED ON CONTEXT RELATIONSHIPS
With our context map complete and relationship patterns identified, we can now design the contracts that will govern how capabilities interact. The DDD relationship patterns directly inform how we design these contracts.
Rationale for this step: Contracts are the mechanism that enables loose coupling between capabilities while respecting the domain relationships identified in the context map. By designing contracts that reflect the DDD patterns, we ensure that our implementation preserves the strategic domain architecture. Different relationship patterns require different contract designs.
How to execute this step: For each relationship identified in the context map, design a contract that implements the appropriate DDD pattern. The pattern determines the contract's characteristics:
- Published Language contracts should be stable, well-documented, and designed for multiple consumers
- Open Host Service contracts should be general-purpose and flexible
- Conformist relationships use contracts as-is without translation
- Anticorruption Layer relationships require translation logic in the Adaptation layer
- Partnership relationships require coordinated evolution of both contracts
- Shared Kernel relationships share contract definitions between capabilities
Let us design contracts for each relationship in our manufacturing system, showing how the DDD pattern influences the design:
/**
* Machine Monitoring Contract - Published Language Pattern
*
* This contract implements the Published Language pattern. It is designed
* to be stable, well-documented, and consumable by multiple downstream
* contexts without requiring them to understand Machine Control internals.
*
* The event schemas are carefully designed to be self-contained and
* meaningful to any consumer interested in machine data.
*/
public interface MachineMonitoringContract {
/**
* PROVISION: Machine Control provides sensor data and machine events
* using a Published Language that downstream contexts can consume
* without translation.
*/
interface Provision {
/**
* Subscribe to sensor data updates for a specific machine.
* The subscriber receives SensorDataEvent objects that are
* part of the published language.
*
* @param machineId The machine to monitor
* @param subscriber The callback for sensor data
* @param updateRateHz Desired update frequency (1-100 Hz)
*/
void subscribeSensorData(
String machineId,
SensorDataSubscriber subscriber,
int updateRateHz
);
/**
* Subscribe to machine state change events.
* Events use the published language's state model.
*/
void subscribeMachineStateChanges(
String machineId,
MachineStateSubscriber subscriber
);
/**
* Subscribe to alarm events.
* Alarm events are part of the published language.
*/
void subscribeAlarms(
AlarmSubscriber subscriber
);
}
/**
* PROTOCOL: Defines the published language event schemas
* and quality of service guarantees.
*/
interface Protocol {
/**
* Get the event schema version.
* Published languages must be versioned carefully.
*/
Version getSchemaVersion();
/**
* Get the maximum supported update rate.
*/
int getMaxUpdateRate();
/**
* Get event delivery guarantees.
* Published languages must specify reliability.
*/
DeliveryGuarantee getDeliveryGuarantee();
}
/**
* Sensor Data Event - part of the Published Language.
* This is a stable, well-documented event schema.
*/
class SensorDataEvent {
private final String machineId;
private final String sensorId;
private final double value;
private final String unit;
private final Instant timestamp;
private final SensorQuality quality;
// Constructor, getters, and documentation
// This is part of the published language, so it must be
// stable and well-documented
}
/**
* Machine State Event - part of the Published Language.
*/
class MachineStateEvent {
private final String machineId;
private final MachineState previousState;
private final MachineState newState;
private final Instant timestamp;
private final String reason;
// Part of published language - stable schema
}
/**
* Alarm Event - part of the Published Language.
*/
class AlarmEvent {
private final String machineId;
private final AlarmSeverity severity;
private final String alarmCode;
private final String description;
private final Instant timestamp;
// Part of published language - stable schema
}
}
/**
* Machine Status Contract - Open Host Service Pattern
*
* This contract implements the Open Host Service pattern. It provides
* a general-purpose service for querying machine status, designed to
* serve multiple different consumers with different needs.
*
* The service is flexible and provides multiple query options to
* accommodate various use cases.
*/
public interface MachineStatusContract {
/**
* PROVISION: Open Host Service for machine status queries
*/
interface Provision {
/**
* Get current status of a specific machine.
* General-purpose query suitable for many consumers.
*/
MachineStatus getMachineStatus(String machineId);
/**
* Get status of all machines.
* Useful for dashboard and monitoring consumers.
*/
List<MachineStatus> getAllMachineStatus();
/**
* Get status of machines matching criteria.
* Flexible query for different consumer needs.
*/
List<MachineStatus> getMachinesByStatus(
MachineState state
);
/**
* Get availability forecast for a machine.
* Supports planning use cases.
*/
AvailabilityForecast getAvailabilityForecast(
String machineId,
Duration forecastPeriod
);
}
/**
* PROTOCOL: Open Host Service quality attributes
*/
interface Protocol {
Version getServiceVersion();
Duration getMaxResponseTime();
int getRateLimitPerMinute();
}
/**
* Machine Status - designed to be useful for many consumers
*/
class MachineStatus {
private final String machineId;
private final MachineState currentState;
private final boolean isAvailable;
private final Optional<String> currentJob;
private final double utilizationPercent;
private final Instant lastStateChange;
// Open Host Service provides rich, general-purpose data
}
}
/**
* Maintenance Schedule Contract - Standard Customer-Supplier
*
* This contract implements a standard Customer-Supplier relationship.
* Equipment Health Monitoring (supplier) provides maintenance schedules
* to Production Planning (customer).
*/
public interface MaintenanceScheduleContract {
interface Provision {
/**
* Get scheduled maintenance for a machine.
*/
List<MaintenanceWindow> getScheduledMaintenance(
String machineId,
LocalDate startDate,
LocalDate endDate
);
/**
* Get all upcoming maintenance across all machines.
*/
List<MaintenanceWindow> getUpcomingMaintenance(
Duration lookahead
);
/**
* Check if a machine is available during a time window.
* Considers scheduled maintenance.
*/
boolean isMachineAvailable(
String machineId,
Instant start,
Instant end
);
}
interface Protocol {
Version getContractVersion();
Duration getDataFreshness();
}
class MaintenanceWindow {
private final String machineId;
private final Instant startTime;
private final Instant endTime;
private final MaintenanceType type;
private final MaintenancePriority priority;
private final boolean isConfirmed;
}
}
/**
* Material Availability Contract - Partnership Pattern (Part 1)
*
* This contract is part of a Partnership relationship between
* Material Management and Production Planning. Both contexts must
* coordinate their evolution of these contracts.
*/
public interface MaterialAvailabilityContract {
/**
* PROVISION: Material Management provides material availability info
* This contract must evolve in coordination with MaterialRequirementContract
*/
interface Provision {
/**
* Check if materials are available for a production order.
*/
MaterialAvailabilityStatus checkAvailability(
MaterialRequirement requirement
);
/**
* Reserve materials for a production order.
* This affects what Production Planning can schedule.
*/
ReservationResult reserveMaterials(
String productionOrderId,
List<MaterialRequirement> requirements
);
/**
* Get projected availability for planning purposes.
*/
MaterialProjection getProjectedAvailability(
String materialId,
LocalDate projectionDate
);
}
interface Protocol {
Version getContractVersion();
/**
* Partnership contracts must specify coordination requirements
*/
CoordinationRequirement getCoordinationRequirement();
}
/**
* Shared type used by both partnership contracts.
* This is part of a Shared Kernel between the two contexts.
*/
class MaterialRequirement {
private final String materialId;
private final double quantity;
private final String unit;
private final LocalDate neededBy;
// This type is shared between both partnership contracts
// Changes must be coordinated
}
}
/**
* Material Requirement Contract - Partnership Pattern (Part 2)
*
* The complementary contract in the Partnership relationship.
* Production Planning provides material requirements to Material Management.
*/
public interface MaterialRequirementContract {
/**
* PROVISION: Production Planning provides material requirements
* This contract must evolve in coordination with MaterialAvailabilityContract
*/
interface Provision {
/**
* Submit material requirements for a production order.
*/
void submitRequirements(
String productionOrderId,
List<MaterialRequirement> requirements
);
/**
* Update requirements for an existing order.
*/
void updateRequirements(
String productionOrderId,
List<MaterialRequirement> updatedRequirements
);
/**
* Get forecasted requirements for planning purposes.
*/
List<MaterialForecast> getForecastedRequirements(
Duration forecastPeriod
);
}
interface Protocol {
Version getContractVersion();
/**
* Must match coordination requirements with partner contract
*/
CoordinationRequirement getCoordinationRequirement();
}
/**
* Uses the shared MaterialRequirement type from the Shared Kernel
*/
class MaterialForecast {
private final MaterialRequirement requirement;
private final double confidence;
private final LocalDate forecastDate;
}
}
Now let us implement an Anticorruption Layer for the legacy SCADA system integration:
/**
* Legacy SCADA Contract - Anticorruption Layer Pattern
*
* This contract represents the legacy SCADA system's interface.
* Machine Control will implement an Anticorruption Layer in its
* Adaptation layer to translate between this contract and its
* clean domain model.
*/
public interface LegacySCADAContract {
/**
* PROVISION: Legacy SCADA provides data in its own format
* This format is not ideal and would corrupt our domain model
* if used directly.
*/
interface Provision {
/**
* Read a tag value from SCADA.
* Tags use cryptic names like "PLC1_AI_001"
*/
SCADATagValue readTag(String tagName);
/**
* Write a tag value to SCADA.
*/
void writeTag(String tagName, Object value);
/**
* Get all tag values for a device.
* Returns a flat map with no structure.
*/
Map<String, Object> getAllTags(String deviceId);
}
/**
* SCADA Tag Value - legacy format
* This is the format we need to protect our domain from
*/
class SCADATagValue {
public String tagName; // Public fields - poor design
public Object value; // Untyped value
public int quality; // Numeric quality code
public long timestamp; // Unix timestamp in milliseconds
public String units; // Sometimes present, sometimes null
// No encapsulation, poor naming, inconsistent data
// This is exactly why we need an Anticorruption Layer
}
}
/**
* SCADA Anticorruption Layer
*
* This class is implemented in the Adaptation layer of Machine Control.
* It translates between the legacy SCADA model and the clean domain model.
*/
public class SCADAAnticorruptionLayer {
private final LegacySCADAContract.Provision scadaSystem;
private final SCADATagMappingRepository tagMappings;
public SCADAAnticorruptionLayer(
LegacySCADAContract.Provision scadaSystem,
SCADATagMappingRepository tagMappings
) {
this.scadaSystem = scadaSystem;
this.tagMappings = tagMappings;
}
/**
* Translate SCADA tag to domain sensor reading.
* This protects our domain model from SCADA's poor design.
*/
public SensorReading readSensor(String machineId, String sensorId) {
// Look up the cryptic SCADA tag name
String tagName = tagMappings.getTagName(machineId, sensorId);
// Read from legacy system
LegacySCADAContract.SCADATagValue tagValue =
scadaSystem.readTag(tagName);
// Translate to clean domain model
return translateToDomainModel(tagValue, machineId, sensorId);
}
/**
* Translate domain actuator command to SCADA tag write.
*/
public void writeActuator(
String machineId,
String actuatorId,
double value
) {
String tagName = tagMappings.getTagName(machineId, actuatorId);
// Translate domain value to SCADA format
Object scadaValue = translateToSCADAFormat(value);
scadaSystem.writeTag(tagName, scadaValue);
}
private SensorReading translateToDomainModel(
LegacySCADAContract.SCADATagValue tagValue,
String machineId,
String sensorId
) {
// Extract and validate data
double value = extractNumericValue(tagValue.value);
SensorQuality quality = translateQuality(tagValue.quality);
Instant timestamp = Instant.ofEpochMilli(tagValue.timestamp);
String unit = tagValue.units != null ? tagValue.units : "unknown";
// Create clean domain object
return new SensorReading(
machineId,
sensorId,
value,
unit,
timestamp,
quality
);
}
private double extractNumericValue(Object value) {
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
throw new SCADATranslationException(
"Tag value is not numeric: " + value
);
}
private SensorQuality translateQuality(int qualityCode) {
// SCADA uses numeric codes, we use enums
switch (qualityCode) {
case 192: return SensorQuality.GOOD;
case 0: return SensorQuality.BAD;
case 64: return SensorQuality.UNCERTAIN;
default: return SensorQuality.UNKNOWN;
}
}
private Object translateToSCADAFormat(double value) {
// SCADA expects specific format
return Double.valueOf(value);
}
}
Content created in this step:
Contract Specifications: For each relationship, a complete contract interface with:
- Provision interface specifying what is provided
- Requirement interface specifying what is needed (if applicable)
- Protocol interface specifying quality attributes and versioning
- Data transfer objects with complete documentation
Pattern Implementation Notes: For each contract, documentation explaining:
- Which DDD pattern it implements
- Why the contract is designed this way
- What constraints the pattern imposes
- How the contract should evolve
Anticorruption Layer Implementations: For relationships using the ACL pattern, complete implementation of the translation logic
Shared Kernel Definitions: For partnerships and shared kernels, identification of shared types and coordination requirements
Published Language Documentation: For published language contracts, comprehensive documentation of event schemas and stability guarantees
STEP FOUR: DESIGNING THE CAPABILITY NUCLEUS STRUCTURE WITH DDD TACTICAL PATTERNS
Now that we have contracts defining how capabilities interact, we can design the internal structure of each capability. This is where we apply the DDD tactical patterns within the CCA Capability Nucleus structure.
Rationale for this step: The three-layer structure of the Capability Nucleus provides a natural home for all DDD tactical patterns. Entities, value objects, aggregates, domain services, and specifications belong in the Essence layer. Repositories and factories that require infrastructure belong in the Realization layer. Application services that orchestrate use cases span the Realization layer. This clear mapping ensures that DDD patterns are applied consistently.
How to execute this step: For each capability identified from the bounded contexts, design its nucleus structure by identifying which DDD tactical patterns apply and where they fit within the three layers.
In the Essence layer, identify:
- Entities: Objects with identity and lifecycle
- Value Objects: Objects defined by their attributes
- Aggregates: Clusters of entities and value objects with consistency boundaries
- Domain Services: Domain logic that doesn't belong to entities
- Specifications: Business rules for selection and validation
- Domain Events: Things that happened in the domain
In the Realization layer, identify:
- Repositories: Persistence mechanisms for aggregates
- Factories: Complex object construction requiring infrastructure
- Application Services: Use case orchestration
- Infrastructure Services: Technical services needed by the domain
In the Adaptation layer, identify:
- Contract Implementations: How this capability implements its provided contracts
- Contract Consumers: How this capability uses required contracts
- Anticorruption Layers: Translation logic for external systems
- Event Publishers/Subscribers: Domain event infrastructure
Let us design the complete nucleus structure for the Equipment Health Monitoring capability, showing where every DDD pattern fits:
/**
* EQUIPMENT HEALTH MONITORING CAPABILITY
*
* Bounded Context: Equipment Health Monitoring
* Purpose: Track equipment condition and predict maintenance needs
*
* This capability demonstrates the complete integration of DDD tactical
* patterns within the CCA Capability Nucleus structure.
*/
// ===================================================================
// ESSENCE LAYER: Pure domain logic with DDD tactical patterns
// ===================================================================
/**
* ENTITY: Equipment
*
* An entity with identity (EquipmentId) and lifecycle.
* This is the aggregate root for the Equipment aggregate.
*/
public class Equipment {
// Identity - distinguishes this entity from others
private final EquipmentId id;
// Value Objects - defined by attributes
private final EquipmentType type;
private final EquipmentSpecification specification;
// Entities within the aggregate
private final List<HealthMetric> metrics;
// Value Object
private MaintenanceSchedule schedule;
// Domain Events - things that happened
private final List<DomainEvent> domainEvents;
/**
* Constructor enforces invariants.
* This is a key responsibility of the aggregate root.
*/
public Equipment(EquipmentId id, EquipmentType type) {
if (id == null) {
throw new IllegalArgumentException("Equipment ID cannot be null");
}
if (type == null) {
throw new IllegalArgumentException("Equipment type cannot be null");
}
this.id = id;
this.type = type;
this.specification = type.getDefaultSpecification();
this.metrics = new ArrayList<>();
this.schedule = MaintenanceSchedule.createDefault(type);
this.domainEvents = new ArrayList<>();
}
/**
* Domain method: Record a health metric.
* Enforces aggregate invariants and raises domain events.
*/
public void recordMetric(HealthMetric metric) {
// Enforce invariant: metric must belong to this equipment
if (!metric.getEquipmentId().equals(this.id)) {
throw new IllegalArgumentException(
"Metric does not belong to this equipment"
);
}
// Enforce invariant: metric must be valid for this equipment type
if (!type.supportsMetric(metric.getMetricName())) {
throw new IllegalArgumentException(
"Equipment type " + type + " does not support metric " +
metric.getMetricName()
);
}
// Add to aggregate
metrics.add(metric);
// Raise domain event
HealthAssessment assessment = assessHealth();
if (assessment.isCritical()) {
domainEvents.add(new CriticalHealthDetectedEvent(
this.id,
assessment,
metric,
Instant.now()
));
}
}
/**
* Domain method: Assess current health.
* Uses Specification pattern for complex business rules.
*/
public HealthAssessment assessHealth() {
if (metrics.isEmpty()) {
return HealthAssessment.unknown();
}
HealthMetric latestMetric = getLatestMetric();
// Use Specification pattern to evaluate health
HealthSpecification criticalSpec =
specification.getCriticalHealthSpecification();
HealthSpecification warningSpec =
specification.getWarningHealthSpecification();
if (criticalSpec.isSatisfiedBy(latestMetric)) {
return HealthAssessment.critical(
"Metric " + latestMetric.getMetricName() +
" is in critical range"
);
}
if (warningSpec.isSatisfiedBy(latestMetric)) {
return HealthAssessment.warning(
"Metric " + latestMetric.getMetricName() +
" is in warning range"
);
}
return HealthAssessment.healthy();
}
/**
* Domain method: Check if maintenance is needed.
* Combines scheduled maintenance with health-based maintenance.
*/
public boolean needsMaintenance(LocalDate currentDate) {
return schedule.isMaintenanceDue(currentDate) ||
assessHealth().requiresImmediateMaintenance();
}
/**
* Domain method: Schedule maintenance.
* Raises domain event.
*/
public void scheduleMaintenance(MaintenanceWindow window) {
schedule = schedule.addWindow(window);
domainEvents.add(new MaintenanceScheduledEvent(
this.id,
window,
Instant.now()
));
}
/**
* Get domain events and clear them.
* This is the standard pattern for collecting domain events.
*/
public List<DomainEvent> getDomainEvents() {
List<DomainEvent> events = new ArrayList<>(domainEvents);
domainEvents.clear();
return events;
}
private HealthMetric getLatestMetric() {
return metrics.get(metrics.size() - 1);
}
// Getters for aggregate root
public EquipmentId getId() {
return id;
}
public EquipmentType getType() {
return type;
}
public MaintenanceSchedule getSchedule() {
return schedule;
}
}
/**
* VALUE OBJECT: EquipmentId
*
* Immutable value object representing equipment identity.
* Value objects are compared by value, not reference.
*/
public class EquipmentId {
private final String value;
public EquipmentId(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(
"Equipment ID cannot be empty"
);
}
this.value = value.trim();
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof EquipmentId)) return false;
EquipmentId other = (EquipmentId) obj;
return value.equals(other.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return "EquipmentId{" + value + "}";
}
}
/**
* VALUE OBJECT: HealthMetric
*
* Immutable value object representing a single health measurement.
*/
public class HealthMetric {
private final EquipmentId equipmentId;
private final String metricName;
private final double value;
private final String unit;
private final Instant timestamp;
public HealthMetric(
EquipmentId equipmentId,
String metricName,
double value,
String unit,
Instant timestamp
) {
this.equipmentId = equipmentId;
this.metricName = metricName;
this.value = value;
this.unit = unit;
this.timestamp = timestamp;
}
// Getters only - immutable
public EquipmentId getEquipmentId() { return equipmentId; }
public String getMetricName() { return metricName; }
public double getValue() { return value; }
public String getUnit() { return unit; }
public Instant getTimestamp() { return timestamp; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof HealthMetric)) return false;
HealthMetric other = (HealthMetric) obj;
return equipmentId.equals(other.equipmentId) &&
metricName.equals(other.metricName) &&
Double.compare(value, other.value) == 0 &&
timestamp.equals(other.timestamp);
}
@Override
public int hashCode() {
return Objects.hash(equipmentId, metricName, value, timestamp);
}
}
/**
* VALUE OBJECT: HealthAssessment
*
* Immutable value object representing health assessment result.
*/
public class HealthAssessment {
private final HealthLevel level;
private final double numericScore;
private final String description;
private HealthAssessment(
HealthLevel level,
double numericScore,
String description
) {
this.level = level;
this.numericScore = numericScore;
this.description = description;
}
// Factory methods for creating assessments
public static HealthAssessment unknown() {
return new HealthAssessment(
HealthLevel.UNKNOWN,
0.0,
"No data available"
);
}
public static HealthAssessment healthy() {
return new HealthAssessment(
HealthLevel.HEALTHY,
100.0,
"All metrics within normal range"
);
}
public static HealthAssessment warning(String reason) {
return new HealthAssessment(
HealthLevel.WARNING,
60.0,
reason
);
}
public static HealthAssessment critical(String reason) {
return new HealthAssessment(
HealthLevel.CRITICAL,
20.0,
reason
);
}
public boolean isCritical() {
return level == HealthLevel.CRITICAL;
}
public boolean isCriticalOrWarning() {
return level == HealthLevel.CRITICAL || level == HealthLevel.WARNING;
}
public boolean requiresImmediateMaintenance() {
return level == HealthLevel.CRITICAL;
}
public HealthLevel getLevel() { return level; }
public double getNumericScore() { return numericScore; }
public String getDescription() { return description; }
}
/**
* SPECIFICATION: HealthSpecification
*
* Encapsulates business rules for evaluating health metrics.
* This is the Specification pattern from DDD.
*/
public interface HealthSpecification {
/**
* Check if the metric satisfies this specification.
*/
boolean isSatisfiedBy(HealthMetric metric);
/**
* Combine specifications with AND logic.
*/
default HealthSpecification and(HealthSpecification other) {
return metric ->
this.isSatisfiedBy(metric) && other.isSatisfiedBy(metric);
}
/**
* Combine specifications with OR logic.
*/
default HealthSpecification or(HealthSpecification other) {
return metric ->
this.isSatisfiedBy(metric) || other.isSatisfiedBy(metric);
}
/**
* Negate this specification.
*/
default HealthSpecification not() {
return metric -> !this.isSatisfiedBy(metric);
}
}
/**
* Concrete Specification: Metric value is above threshold
*/
public class MetricAboveThresholdSpecification
implements HealthSpecification {
private final String metricName;
private final double threshold;
public MetricAboveThresholdSpecification(
String metricName,
double threshold
) {
this.metricName = metricName;
this.threshold = threshold;
}
@Override
public boolean isSatisfiedBy(HealthMetric metric) {
return metric.getMetricName().equals(metricName) &&
metric.getValue() > threshold;
}
}
/**
* Concrete Specification: Metric value is below threshold
*/
public class MetricBelowThresholdSpecification
implements HealthSpecification {
private final String metricName;
private final double threshold;
public MetricBelowThresholdSpecification(
String metricName,
double threshold
) {
this.metricName = metricName;
this.threshold = threshold;
}
@Override
public boolean isSatisfiedBy(HealthMetric metric) {
return metric.getMetricName().equals(metricName) &&
metric.getValue() < threshold;
}
}
/**
* DOMAIN SERVICE: MaintenancePredictionService
*
* Domain logic that doesn't naturally belong to any entity.
* Domain services are stateless and operate on domain objects.
*/
public class MaintenancePredictionService {
/**
* Predict when maintenance will be needed.
* This is domain logic that requires multiple aggregates
* and historical data, so it's a domain service.
*/
public PredictedMaintenanceDate predictNextMaintenance(
Equipment equipment,
List<HistoricalMaintenanceRecord> history
) {
HealthAssessment currentHealth = equipment.assessHealth();
// If already critical, maintenance is immediate
if (currentHealth.isCritical()) {
return PredictedMaintenanceDate.immediate();
}
// Calculate degradation rate from history
double degradationRate = calculateDegradationRate(history);
// Predict based on current health and degradation rate
if (degradationRate > 0) {
int daysUntilCritical = estimateDaysUntilCritical(
currentHealth,
degradationRate
);
return PredictedMaintenanceDate.inDays(daysUntilCritical);
}
// Fall back to scheduled maintenance
return PredictedMaintenanceDate.onDate(
equipment.getSchedule().getNextScheduledDate()
);
}
/**
* Calculate equipment degradation rate.
* Pure domain logic with no infrastructure dependencies.
*/
private double calculateDegradationRate(
List<HistoricalMaintenanceRecord> history
) {
if (history.size() < 2) {
return 0.0;
}
double totalDegradation = 0.0;
for (int i = 1; i < history.size(); i++) {
double scoreDiff =
history.get(i).getHealthScore() -
history.get(i-1).getHealthScore();
totalDegradation += scoreDiff;
}
return totalDegradation / (history.size() - 1);
}
/**
* Estimate days until critical condition.
* Pure domain calculation.
*/
private int estimateDaysUntilCritical(
HealthAssessment current,
double degradationRate
) {
double currentScore = current.getNumericScore();
double criticalThreshold = 20.0;
double remainingMargin = currentScore - criticalThreshold;
if (degradationRate <= 0) {
return Integer.MAX_VALUE;
}
return (int)(remainingMargin / degradationRate);
}
}
/**
* DOMAIN EVENT: CriticalHealthDetectedEvent
*
* Represents something that happened in the domain.
* Domain events are immutable value objects.
*/
public class CriticalHealthDetectedEvent implements DomainEvent {
private final EquipmentId equipmentId;
private final HealthAssessment assessment;
private final HealthMetric triggeringMetric;
private final Instant occurredAt;
public CriticalHealthDetectedEvent(
EquipmentId equipmentId,
HealthAssessment assessment,
HealthMetric triggeringMetric,
Instant occurredAt
) {
this.equipmentId = equipmentId;
this.assessment = assessment;
this.triggeringMetric = triggeringMetric;
this.occurredAt = occurredAt;
}
@Override
public Instant occurredAt() {
return occurredAt;
}
// Getters
public EquipmentId getEquipmentId() { return equipmentId; }
public HealthAssessment getAssessment() { return assessment; }
public HealthMetric getTriggeringMetric() { return triggeringMetric; }
}
// ===================================================================
// REALIZATION LAYER: Infrastructure integration with DDD patterns
// ===================================================================
/**
* REPOSITORY: EquipmentRepository
*
* Provides collection-like interface to Equipment aggregates.
* Repositories are implemented in the Realization layer because
* they require infrastructure (database).
*/
public interface EquipmentRepository {
/**
* Find equipment by ID.
* Returns Optional because equipment might not exist.
*/
Optional<Equipment> findById(EquipmentId id);
/**
* Find all equipment.
* Repository provides collection-like interface.
*/
List<Equipment> findAll();
/**
* Find equipment by specification.
* Combines Repository and Specification patterns.
*/
List<Equipment> findBySpecification(
EquipmentSpecification spec
);
/**
* Save equipment aggregate.
* Repository handles persistence of entire aggregate.
*/
void save(Equipment equipment);
/**
* Delete equipment aggregate.
*/
void delete(EquipmentId id);
}
/**
* REPOSITORY IMPLEMENTATION: JpaEquipmentRepository
*
* Concrete implementation using JPA for persistence.
* This is in the Realization layer.
*/
public class JpaEquipmentRepository implements EquipmentRepository {
private final EntityManager entityManager;
public JpaEquipmentRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Optional<Equipment> findById(EquipmentId id) {
EquipmentJpaEntity entity = entityManager.find(
EquipmentJpaEntity.class,
id.getValue()
);
if (entity == null) {
return Optional.empty();
}
return Optional.of(toDomainModel(entity));
}
@Override
public List<Equipment> findAll() {
List<EquipmentJpaEntity> entities = entityManager
.createQuery(
"SELECT e FROM EquipmentJpaEntity e",
EquipmentJpaEntity.class
)
.getResultList();
return entities.stream()
.map(this::toDomainModel)
.collect(Collectors.toList());
}
@Override
public void save(Equipment equipment) {
EquipmentJpaEntity entity = toJpaEntity(equipment);
if (entityManager.find(
EquipmentJpaEntity.class,
equipment.getId().getValue()
) == null) {
entityManager.persist(entity);
} else {
entityManager.merge(entity);
}
}
/**
* Translate from JPA entity to domain model.
* This is the repository's responsibility.
*/
private Equipment toDomainModel(EquipmentJpaEntity entity) {
Equipment equipment = new Equipment(
new EquipmentId(entity.getId()),
EquipmentType.valueOf(entity.getType())
);
// Load metrics
for (HealthMetricJpaEntity metricEntity : entity.getMetrics()) {
equipment.recordMetric(new HealthMetric(
new EquipmentId(metricEntity.getEquipmentId()),
metricEntity.getMetricName(),
metricEntity.getValue(),
metricEntity.getUnit(),
metricEntity.getTimestamp()
));
}
return equipment;
}
/**
* Translate from domain model to JPA entity.
*/
private EquipmentJpaEntity toJpaEntity(Equipment equipment) {
EquipmentJpaEntity entity = new EquipmentJpaEntity();
entity.setId(equipment.getId().getValue());
entity.setType(equipment.getType().name());
// Save metrics
// Note: In real implementation, we'd handle this more carefully
// to avoid loading all metrics every time
return entity;
}
}
/**
* APPLICATION SERVICE: EquipmentHealthApplicationService
*
* Orchestrates use cases and transactions.
* Application services are in the Realization layer.
*/
public class EquipmentHealthApplicationService {
private final EquipmentRepository equipmentRepository;
private final MaintenancePredictionService predictionService;
private final DomainEventPublisher eventPublisher;
public EquipmentHealthApplicationService(
EquipmentRepository equipmentRepository,
MaintenancePredictionService predictionService,
DomainEventPublisher eventPublisher
) {
this.equipmentRepository = equipmentRepository;
this.predictionService = predictionService;
this.eventPublisher = eventPublisher;
}
/**
* Use case: Record health metric.
* Application service orchestrates the use case.
*/
@Transactional
public void recordHealthMetric(
String equipmentId,
String metricName,
double value,
String unit,
Instant timestamp
) {
// Load aggregate from repository
Equipment equipment = equipmentRepository
.findById(new EquipmentId(equipmentId))
.orElseThrow(() -> new EquipmentNotFoundException(equipmentId));
// Create value object
HealthMetric metric = new HealthMetric(
new EquipmentId(equipmentId),
metricName,
value,
unit,
timestamp
);
// Execute domain logic
equipment.recordMetric(metric);
// Save aggregate
equipmentRepository.save(equipment);
// Publish domain events
List<DomainEvent> events = equipment.getDomainEvents();
for (DomainEvent event : events) {
eventPublisher.publish(event);
}
}
/**
* Use case: Get health assessment.
*/
public HealthAssessment getHealthAssessment(String equipmentId) {
Equipment equipment = equipmentRepository
.findById(new EquipmentId(equipmentId))
.orElseThrow(() -> new EquipmentNotFoundException(equipmentId));
return equipment.assessHealth();
}
/**
* Use case: Predict maintenance.
* Uses domain service for prediction logic.
*/
public PredictedMaintenanceDate predictMaintenance(
String equipmentId
) {
Equipment equipment = equipmentRepository
.findById(new EquipmentId(equipmentId))
.orElseThrow(() -> new EquipmentNotFoundException(equipmentId));
List<HistoricalMaintenanceRecord> history =
loadMaintenanceHistory(equipmentId);
// Delegate to domain service
return predictionService.predictNextMaintenance(
equipment,
history
);
}
private List<HistoricalMaintenanceRecord> loadMaintenanceHistory(
String equipmentId
) {
// Load from database
// Simplified for example
return new ArrayList<>();
}
}
// ===================================================================
// ADAPTATION LAYER: Contracts and external interfaces
// ===================================================================
/**
* CONTRACT IMPLEMENTATION: MaintenanceScheduleContract
*
* The Adaptation layer implements the contracts that this capability provides.
*/
public class MaintenanceScheduleContractImpl
implements MaintenanceScheduleContract.Provision {
private final EquipmentHealthApplicationService applicationService;
private final EquipmentRepository equipmentRepository;
public MaintenanceScheduleContractImpl(
EquipmentHealthApplicationService applicationService,
EquipmentRepository equipmentRepository
) {
this.applicationService = applicationService;
this.equipmentRepository = equipmentRepository;
}
@Override
public List<MaintenanceWindow> getScheduledMaintenance(
String machineId,
LocalDate startDate,
LocalDate endDate
) {
Equipment equipment = equipmentRepository
.findById(new EquipmentId(machineId))
.orElseThrow(() -> new EquipmentNotFoundException(machineId));
// Convert domain model to contract DTO
return equipment.getSchedule()
.getWindowsBetween(startDate, endDate)
.stream()
.map(this::toContractDTO)
.collect(Collectors.toList());
}
@Override
public List<MaintenanceWindow> getUpcomingMaintenance(
Duration lookahead
) {
LocalDate endDate = LocalDate.now().plus(lookahead);
List<Equipment> allEquipment = equipmentRepository.findAll();
return allEquipment.stream()
.flatMap(eq -> eq.getSchedule()
.getWindowsBetween(LocalDate.now(), endDate)
.stream())
.map(this::toContractDTO)
.collect(Collectors.toList());
}
@Override
public boolean isMachineAvailable(
String machineId,
Instant start,
Instant end
) {
Equipment equipment = equipmentRepository
.findById(new EquipmentId(machineId))
.orElseThrow(() -> new EquipmentNotFoundException(machineId));
return equipment.getSchedule().isAvailableDuring(start, end);
}
/**
* Translate domain model to contract DTO.
* This is the Adaptation layer's responsibility.
*/
private MaintenanceWindow toContractDTO(
com.manufacturing.domain.MaintenanceWindow domainWindow
) {
return new MaintenanceWindow(
domainWindow.getEquipmentId().getValue(),
domainWindow.getStartTime(),
domainWindow.getEndTime(),
domainWindow.getType(),
domainWindow.getPriority(),
domainWindow.isConfirmed()
);
}
}
/**
* CONTRACT CONSUMER: MachineMonitoringContractConsumer
*
* The Adaptation layer consumes contracts from other capabilities.
* This implements the Conformist pattern - we use the contract as-is.
*/
public class MachineMonitoringContractConsumer {
private final EquipmentHealthApplicationService applicationService;
private MachineMonitoringContract.Provision machineMonitoring;
public MachineMonitoringContractConsumer(
EquipmentHealthApplicationService applicationService
) {
this.applicationService = applicationService;
}
/**
* This method is called by the capability registry to inject
* the required contract.
*/
public void setMachineMonitoring(
MachineMonitoringContract.Provision machineMonitoring
) {
this.machineMonitoring = machineMonitoring;
// Subscribe to sensor data for all machines
// In real implementation, we'd get machine IDs from configuration
subscribeTo("MACHINE-001");
subscribeTo("MACHINE-002");
}
private void subscribeTo(String machineId) {
machineMonitoring.subscribeSensorData(
machineId,
this::handleSensorData,
1 // 1 Hz update rate
);
}
/**
* Handle sensor data events.
* Translate contract DTO to domain model and invoke application service.
*/
private void handleSensorData(
MachineMonitoringContract.SensorDataEvent event
) {
// Translate contract event to domain model
String equipmentId = event.getMachineId();
String metricName = "sensor-" + event.getSensorId();
double value = event.getValue();
String unit = event.getUnit();
Instant timestamp = event.getTimestamp();
// Invoke application service
applicationService.recordHealthMetric(
equipmentId,
metricName,
value,
unit,
timestamp
);
}
}
Content created in this step:
Essence Layer Design: Complete implementation of all DDD tactical patterns in the Essence layer:
- Entities with identity and lifecycle
- Value objects that are immutable and compared by value
- Aggregates with roots that enforce invariants
- Domain services for logic that doesn't belong to entities
- Specifications for complex business rules
- Domain events for things that happened
Realization Layer Design: Implementation of infrastructure-dependent DDD patterns:
- Repositories for aggregate persistence
- Application services for use case orchestration
- Domain event publishers
- Transaction management
Adaptation Layer Design: Contract implementations and consumers:
- Contract provision implementations
- Contract requirement consumers
- DTO translation logic
- Event subscription handling
Pattern Placement Guide: Documentation showing exactly where each DDD pattern belongs within the CCA structure
Complete Working Example: A fully implemented capability showing all patterns working together
STEP FIVE: IMPLEMENTING DOMAIN MODELS IN THE ESSENCE LAYER WITH ALL DDD TACTICAL PATTERNS
With the nucleus structure designed, we can now implement the complete domain model within the Essence layer of each capability, applying all relevant DDD tactical patterns. This step brings the domain model to life in code.
Rationale for this step: The Essence layer is where the domain model lives in its purest form. By implementing all DDD tactical patterns (entities, value objects, aggregates, domain services, specifications, factories, domain events) in the Essence, we create a clear representation of the domain that is independent of technical concerns. This makes the domain model easier to understand, discuss with domain experts, test thoroughly, and evolve as domain understanding deepens.
How to execute this step: For each capability, take the domain model from its bounded context and implement it in the Essence layer using the appropriate DDD tactical patterns. Follow these guidelines:
Entities: Implement as classes with identity fields, mutable state, and methods that represent domain operations. Ensure each entity has a clear identity that distinguishes it from other instances.
Value Objects: Implement as immutable classes with no identity. Override equals() and hashCode() to compare by value. Make all fields final and provide no setters.
Aggregates: Identify aggregate boundaries based on consistency requirements. Implement the aggregate root as an entity that enforces invariants across the entire aggregate. Ensure external code can only access the aggregate through the root.
Domain Services: Implement as stateless services for domain logic that doesn't naturally belong to any entity. Domain services should operate on domain objects passed as parameters and should have no infrastructure dependencies.
Specifications: Implement the Specification pattern for complex business rules, especially those used for selection or validation. Specifications should be combinable using AND, OR, and NOT operations.
Factories: Implement factory methods or classes for complex object construction. Simple factories can be static methods on the entity or value object. Complex factories that require infrastructure belong in the Realization layer.
Domain Events: Implement as immutable value objects representing things that happened in the domain. Entities should collect domain events and make them available for publishing.
Let us implement a complete domain model for the Production Planning capability:
// ===================================================================
// PRODUCTION PLANNING CAPABILITY - ESSENCE LAYER
// Complete DDD Tactical Patterns Implementation
// ===================================================================
/**
* AGGREGATE ROOT: ProductionOrder
*
* This is the aggregate root for the Production Order aggregate.
* It enforces invariants across the entire aggregate including
* production order lines, resource allocations, and schedule.
*/
public class ProductionOrder {
// IDENTITY
private final ProductionOrderId id;
// VALUE OBJECTS
private final ProductCode productCode;
private final Quantity targetQuantity;
private final ProductionPriority priority;
// ENTITIES WITHIN AGGREGATE
private final List<ProductionOrderLine> orderLines;
private final List<ResourceAllocation> resourceAllocations;
// VALUE OBJECT
private ProductionSchedule schedule;
// STATE
private ProductionOrderStatus status;
// DOMAIN EVENTS
private final List<DomainEvent> domainEvents;
/**
* FACTORY METHOD: Create a new production order.
* This is a simple factory implemented as a static method.
*/
public static ProductionOrder create(
ProductionOrderId id,
ProductCode productCode,
Quantity targetQuantity,
ProductionPriority priority,
LocalDateTime requestedCompletionDate
) {
ProductionOrder order = new ProductionOrder(
id,
productCode,
targetQuantity,
priority
);
order.domainEvents.add(new ProductionOrderCreatedEvent(
id,
productCode,
targetQuantity,
Instant.now()
));
return order;
}
/**
* Private constructor enforces use of factory method.
*/
private ProductionOrder(
ProductionOrderId id,
ProductCode productCode,
Quantity targetQuantity,
ProductionPriority priority
) {
// Enforce invariants
if (id == null) {
throw new IllegalArgumentException("Order ID cannot be null");
}
if (targetQuantity.isZeroOrNegative()) {
throw new IllegalArgumentException(
"Target quantity must be positive"
);
}
this.id = id;
this.productCode = productCode;
this.targetQuantity = targetQuantity;
this.priority = priority;
this.orderLines = new ArrayList<>();
this.resourceAllocations = new ArrayList<>();
this.status = ProductionOrderStatus.CREATED;
this.domainEvents = new ArrayList<>();
}
/**
* DOMAIN METHOD: Add a production step.
* Enforces aggregate invariants.
*/
public void addProductionStep(
ProductionStep step,
Duration estimatedDuration,
List<ResourceRequirement> resourceRequirements
) {
// Enforce invariant: can only add steps when order is in CREATED status
if (status != ProductionOrderStatus.CREATED) {
throw new IllegalStateException(
"Cannot add steps to order in status " + status
);
}
// Create order line (entity within aggregate)
ProductionOrderLine line = new ProductionOrderLine(
ProductionOrderLineId.generate(),
this.id,
step,
estimatedDuration,
resourceRequirements
);
orderLines.add(line);
domainEvents.add(new ProductionStepAddedEvent(
this.id,
step,
Instant.now()
));
}
/**
* DOMAIN METHOD: Schedule the production order.
* Uses a DOMAIN SERVICE for complex scheduling logic.
*/
public void schedule(
ProductionSchedulingService schedulingService,
List<MachineAvailability> machineAvailability,
List<MaterialAvailability> materialAvailability
) {
// Enforce invariant: can only schedule if not already scheduled
if (status == ProductionOrderStatus.SCHEDULED ||
status == ProductionOrderStatus.IN_PROGRESS ||
status == ProductionOrderStatus.COMPLETED) {
throw new IllegalStateException(
"Order is already scheduled or in progress"
);
}
// Delegate complex scheduling logic to domain service
ProductionSchedule newSchedule = schedulingService.createSchedule(
this,
machineAvailability,
materialAvailability
);
// Enforce invariant: schedule must be feasible
if (!newSchedule.isFeasible()) {
throw new SchedulingInfeasibleException(
"Cannot create feasible schedule for order " + id
);
}
this.schedule = newSchedule;
this.status = ProductionOrderStatus.SCHEDULED;
// Allocate resources based on schedule
allocateResources(newSchedule);
domainEvents.add(new ProductionOrderScheduledEvent(
this.id,
newSchedule.getStartTime(),
newSchedule.getEndTime(),
Instant.now()
));
}
/**
* DOMAIN METHOD: Start production.
*/
public void startProduction() {
// Enforce invariant: must be scheduled before starting
if (status != ProductionOrderStatus.SCHEDULED) {
throw new IllegalStateException(
"Cannot start production for order in status " + status
);
}
// Enforce invariant: all resources must be allocated
if (!areAllResourcesAllocated()) {
throw new IllegalStateException(
"Cannot start production without all resources allocated"
);
}
this.status = ProductionOrderStatus.IN_PROGRESS;
domainEvents.add(new ProductionStartedEvent(
this.id,
Instant.now()
));
}
/**
* DOMAIN METHOD: Complete a production step.
*/
public void completeStep(ProductionOrderLineId lineId, Quantity producedQuantity) {
ProductionOrderLine line = findLine(lineId);
if (line == null) {
throw new IllegalArgumentException(
"Line " + lineId + " not found in order " + this.id
);
}
line.complete(producedQuantity);
// Check if all steps are complete
if (areAllStepsComplete()) {
completeOrder();
}
domainEvents.add(new ProductionStepCompletedEvent(
this.id,
lineId,
producedQuantity,
Instant.now()
));
}
/**
* DOMAIN METHOD: Check if order can be scheduled.
* Uses SPECIFICATION pattern.
*/
public boolean canBeScheduled(ProductionOrderSpecification specification) {
return specification.isSatisfiedBy(this);
}
/**
* Private helper: Allocate resources based on schedule.
*/
private void allocateResources(ProductionSchedule schedule) {
for (ProductionOrderLine line : orderLines) {
for (ResourceRequirement requirement : line.getResourceRequirements()) {
ResourceAllocation allocation = new ResourceAllocation(
ResourceAllocationId.generate(),
this.id,
requirement.getResourceType(),
requirement.getQuantity(),
schedule.getStartTimeForStep(line.getId()),
schedule.getEndTimeForStep(line.getId())
);
resourceAllocations.add(allocation);
}
}
}
private boolean areAllResourcesAllocated() {
return resourceAllocations.stream()
.allMatch(ResourceAllocation::isConfirmed);
}
private boolean areAllStepsComplete() {
return orderLines.stream()
.allMatch(ProductionOrderLine::isComplete);
}
private void completeOrder() {
this.status = ProductionOrderStatus.COMPLETED;
domainEvents.add(new ProductionOrderCompletedEvent(
this.id,
calculateTotalProducedQuantity(),
Instant.now()
));
}
private Quantity calculateTotalProducedQuantity() {
return orderLines.stream()
.map(ProductionOrderLine::getProducedQuantity)
.reduce(Quantity.zero(), Quantity::add);
}
private ProductionOrderLine findLine(ProductionOrderLineId lineId) {
return orderLines.stream()
.filter(line -> line.getId().equals(lineId))
.findFirst()
.orElse(null);
}
// Getters
public ProductionOrderId getId() { return id; }
public ProductCode getProductCode() { return productCode; }
public Quantity getTargetQuantity() { return targetQuantity; }
public ProductionPriority getPriority() { return priority; }
public ProductionOrderStatus getStatus() { return status; }
public ProductionSchedule getSchedule() { return schedule; }
public List<ProductionOrderLine> getOrderLines() {
return Collections.unmodifiableList(orderLines);
}
public List<DomainEvent> getDomainEvents() {
List<DomainEvent> events = new ArrayList<>(domainEvents);
domainEvents.clear();
return events;
}
}
/**
* ENTITY (within aggregate): ProductionOrderLine
*
* This is an entity within the ProductionOrder aggregate.
* It can only be accessed through the aggregate root.
*/
class ProductionOrderLine {
private final ProductionOrderLineId id;
private final ProductionOrderId orderId;
private final ProductionStep step;
private final Duration estimatedDuration;
private final List<ResourceRequirement> resourceRequirements;
private Quantity producedQuantity;
private boolean complete;
ProductionOrderLine(
ProductionOrderLineId id,
ProductionOrderId orderId,
ProductionStep step,
Duration estimatedDuration,
List<ResourceRequirement> resourceRequirements
) {
this.id = id;
this.orderId = orderId;
this.step = step;
this.estimatedDuration = estimatedDuration;
this.resourceRequirements = new ArrayList<>(resourceRequirements);
this.producedQuantity = Quantity.zero();
this.complete = false;
}
void complete(Quantity producedQuantity) {
this.producedQuantity = producedQuantity;
this.complete = true;
}
boolean isComplete() {
return complete;
}
ProductionOrderLineId getId() { return id; }
ProductionStep getStep() { return step; }
Duration getEstimatedDuration() { return estimatedDuration; }
List<ResourceRequirement> getResourceRequirements() {
return Collections.unmodifiableList(resourceRequirements);
}
Quantity getProducedQuantity() { return producedQuantity; }
}
/**
* VALUE OBJECT: ProductionOrderId
*
* Immutable identifier for production orders.
*/
public class ProductionOrderId {
private final String value;
public ProductionOrderId(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Order ID cannot be empty");
}
this.value = value.trim();
}
public static ProductionOrderId generate() {
return new ProductionOrderId("PO-" + UUID.randomUUID().toString());
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof ProductionOrderId)) return false;
ProductionOrderId other = (ProductionOrderId) obj;
return value.equals(other.value);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return value;
}
}
/**
* VALUE OBJECT: Quantity
*
* Represents a quantity with unit.
* Immutable value object with domain operations.
*/
public class Quantity {
private final double amount;
private final String unit;
public Quantity(double amount, String unit) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (unit == null || unit.trim().isEmpty()) {
throw new IllegalArgumentException("Unit cannot be empty");
}
this.amount = amount;
this.unit = unit.trim();
}
public static Quantity zero() {
return new Quantity(0, "units");
}
/**
* Domain operation: Add quantities.
* Enforces that units must match.
*/
public Quantity add(Quantity other) {
if (!this.unit.equals(other.unit)) {
throw new IllegalArgumentException(
"Cannot add quantities with different units: " +
this.unit + " and " + other.unit
);
}
return new Quantity(this.amount + other.amount, this.unit);
}
/**
* Domain operation: Subtract quantities.
*/
public Quantity subtract(Quantity other) {
if (!this.unit.equals(other.unit)) {
throw new IllegalArgumentException(
"Cannot subtract quantities with different units"
);
}
return new Quantity(this.amount - other.amount, this.unit);
}
/**
* Domain operation: Multiply by scalar.
*/
public Quantity multiply(double factor) {
return new Quantity(this.amount * factor, this.unit);
}
/**
* Domain query: Check if zero or negative.
*/
public boolean isZeroOrNegative() {
return amount <= 0;
}
/**
* Domain query: Check if greater than another quantity.
*/
public boolean isGreaterThan(Quantity other) {
if (!this.unit.equals(other.unit)) {
throw new IllegalArgumentException(
"Cannot compare quantities with different units"
);
}
return this.amount > other.amount;
}
public double getAmount() { return amount; }
public String getUnit() { return unit; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Quantity)) return false;
Quantity other = (Quantity) obj;
return Double.compare(amount, other.amount) == 0 &&
unit.equals(other.unit);
}
@Override
public int hashCode() {
return Objects.hash(amount, unit);
}
@Override
public String toString() {
return amount + " " + unit;
}
}
/**
* VALUE OBJECT: ProductionSchedule
*
* Represents the schedule for a production order.
* Immutable value object.
*/
public class ProductionSchedule {
private final Map<ProductionOrderLineId, ScheduledStep> scheduledSteps;
private final Instant startTime;
private final Instant endTime;
private final boolean feasible;
public ProductionSchedule(
Map<ProductionOrderLineId, ScheduledStep> scheduledSteps,
boolean feasible
) {
this.scheduledSteps = new HashMap<>(scheduledSteps);
this.feasible = feasible;
// Calculate overall start and end times
this.startTime = scheduledSteps.values().stream()
.map(ScheduledStep::getStartTime)
.min(Instant::compareTo)
.orElse(Instant.now());
this.endTime = scheduledSteps.values().stream()
.map(ScheduledStep::getEndTime)
.max(Instant::compareTo)
.orElse(Instant.now());
}
public boolean isFeasible() {
return feasible;
}
public Instant getStartTime() {
return startTime;
}
public Instant getEndTime() {
return endTime;
}
public Instant getStartTimeForStep(ProductionOrderLineId lineId) {
ScheduledStep step = scheduledSteps.get(lineId);
return step != null ? step.getStartTime() : null;
}
public Instant getEndTimeForStep(ProductionOrderLineId lineId) {
ScheduledStep step = scheduledSteps.get(lineId);
return step != null ? step.getEndTime() : null;
}
/**
* Value object within value object
*/
public static class ScheduledStep {
private final Instant startTime;
private final Instant endTime;
private final String machineId;
public ScheduledStep(
Instant startTime,
Instant endTime,
String machineId
) {
this.startTime = startTime;
this.endTime = endTime;
this.machineId = machineId;
}
public Instant getStartTime() { return startTime; }
public Instant getEndTime() { return endTime; }
public String getMachineId() { return machineId; }
}
}
/**
* DOMAIN SERVICE: ProductionSchedulingService
*
* Complex scheduling logic that doesn't belong to any entity.
* Stateless service that operates on domain objects.
*/
public class ProductionSchedulingService {
/**
* Create a production schedule for an order.
* This is complex domain logic that requires multiple inputs
* and doesn't naturally belong to the ProductionOrder entity.
*/
public ProductionSchedule createSchedule(
ProductionOrder order,
List<MachineAvailability> machineAvailability,
List<MaterialAvailability> materialAvailability
) {
Map<ProductionOrderLineId, ProductionSchedule.ScheduledStep> scheduledSteps =
new HashMap<>();
Instant currentTime = Instant.now();
boolean allStepsFeasible = true;
// Schedule each production step
for (ProductionOrderLine line : order.getOrderLines()) {
// Find available machine for this step
Optional<MachineAvailability> availableMachine =
findAvailableMachine(
line.getStep(),
machineAvailability,
currentTime,
line.getEstimatedDuration()
);
if (!availableMachine.isPresent()) {
allStepsFeasible = false;
continue;
}
// Check material availability
if (!areMaterialsAvailable(
line.getResourceRequirements(),
materialAvailability,
currentTime
)) {
allStepsFeasible = false;
continue;
}
// Schedule this step
Instant stepStart = currentTime;
Instant stepEnd = stepStart.plus(line.getEstimatedDuration());
scheduledSteps.put(
line.getId(),
new ProductionSchedule.ScheduledStep(
stepStart,
stepEnd,
availableMachine.get().getMachineId()
)
);
// Next step starts after this one ends (simplified)
currentTime = stepEnd;
}
return new ProductionSchedule(scheduledSteps, allStepsFeasible);
}
/**
* Find an available machine for a production step.
* Pure domain logic.
*/
private Optional<MachineAvailability> findAvailableMachine(
ProductionStep step,
List<MachineAvailability> availability,
Instant requiredStart,
Duration duration
) {
return availability.stream()
.filter(machine -> machine.canPerformStep(step))
.filter(machine -> machine.isAvailableDuring(
requiredStart,
requiredStart.plus(duration)
))
.findFirst();
}
/**
* Check if materials are available.
* Pure domain logic.
*/
private boolean areMaterialsAvailable(
List<ResourceRequirement> requirements,
List<MaterialAvailability> availability,
Instant requiredTime
) {
for (ResourceRequirement requirement : requirements) {
boolean found = availability.stream()
.anyMatch(material ->
material.getMaterialId().equals(requirement.getResourceType()) &&
material.getAvailableQuantity().isGreaterThan(requirement.getQuantity()) &&
material.isAvailableAt(requiredTime)
);
if (!found) {
return false;
}
}
return true;
}
}
/**
* SPECIFICATION: ProductionOrderSpecification
*
* Encapsulates business rules for production orders.
* Specifications can be combined using AND, OR, NOT.
*/
public interface ProductionOrderSpecification {
boolean isSatisfiedBy(ProductionOrder order);
default ProductionOrderSpecification and(
ProductionOrderSpecification other
) {
return order ->
this.isSatisfiedBy(order) && other.isSatisfiedBy(order);
}
default ProductionOrderSpecification or(
ProductionOrderSpecification other
) {
return order ->
this.isSatisfiedBy(order) || other.isSatisfiedBy(order);
}
default ProductionOrderSpecification not() {
return order -> !this.isSatisfiedBy(order);
}
}
/**
* Concrete Specification: Order has high priority
*/
public class HighPriorityOrderSpecification
implements ProductionOrderSpecification {
@Override
public boolean isSatisfiedBy(ProductionOrder order) {
return order.getPriority() == ProductionPriority.HIGH ||
order.getPriority() == ProductionPriority.URGENT;
}
}
/**
* Concrete Specification: Order can be completed by date
*/
public class CompletableByDateSpecification
implements ProductionOrderSpecification {
private final LocalDateTime requiredDate;
private final ProductionSchedulingService schedulingService;
private final List<MachineAvailability> machineAvailability;
private final List<MaterialAvailability> materialAvailability;
public CompletableByDateSpecification(
LocalDateTime requiredDate,
ProductionSchedulingService schedulingService,
List<MachineAvailability> machineAvailability,
List<MaterialAvailability> materialAvailability
) {
this.requiredDate = requiredDate;
this.schedulingService = schedulingService;
this.machineAvailability = machineAvailability;
this.materialAvailability = materialAvailability;
}
@Override
public boolean isSatisfiedBy(ProductionOrder order) {
ProductionSchedule schedule = schedulingService.createSchedule(
order,
machineAvailability,
materialAvailability
);
if (!schedule.isFeasible()) {
return false;
}
LocalDateTime completionDate = LocalDateTime.ofInstant(
schedule.getEndTime(),
ZoneId.systemDefault()
);
return completionDate.isBefore(requiredDate) ||
completionDate.isEqual(requiredDate);
}
}
/**
* DOMAIN EVENT: ProductionOrderCreatedEvent
*
* Immutable value object representing something that happened.
*/
public class ProductionOrderCreatedEvent implements DomainEvent {
private final ProductionOrderId orderId;
private final ProductCode productCode;
private final Quantity targetQuantity;
private final Instant occurredAt;
public ProductionOrderCreatedEvent(
ProductionOrderId orderId,
ProductCode productCode,
Quantity targetQuantity,
Instant occurredAt
) {
this.orderId = orderId;
this.productCode = productCode;
this.targetQuantity = targetQuantity;
this.occurredAt = occurredAt;
}
@Override
public Instant occurredAt() {
return occurredAt;
}
public ProductionOrderId getOrderId() { return orderId; }
public ProductCode getProductCode() { return productCode; }
public Quantity getTargetQuantity() { return targetQuantity; }
}
/**
* DOMAIN EVENT: ProductionOrderScheduledEvent
*/
public class ProductionOrderScheduledEvent implements DomainEvent {
private final ProductionOrderId orderId;
private final Instant scheduledStart;
private final Instant scheduledEnd;
private final Instant occurredAt;
public ProductionOrderScheduledEvent(
ProductionOrderId orderId,
Instant scheduledStart,
Instant scheduledEnd,
Instant occurredAt
) {
this.orderId = orderId;
this.scheduledStart = scheduledStart;
this.scheduledEnd = scheduledEnd;
this.occurredAt = occurredAt;
}
@Override
public Instant occurredAt() {
return occurredAt;
}
public ProductionOrderId getOrderId() { return orderId; }
public Instant getScheduledStart() { return scheduledStart; }
public Instant getScheduledEnd() { return scheduledEnd; }
}
This comprehensive example demonstrates all DDD tactical patterns implemented in the Essence layer:
- Entities with identity (ProductionOrder, ProductionOrderLine)
- Value Objects that are immutable (ProductionOrderId, Quantity, ProductionSchedule)
- Aggregates with roots that enforce invariants (ProductionOrder is the aggregate root)
- Domain Services for complex logic (ProductionSchedulingService)
- Specifications for business rules (ProductionOrderSpecification and concrete implementations)
- Factory Methods for object creation (ProductionOrder.create())
- Domain Events for things that happened (ProductionOrderCreatedEvent, etc.)
Content created in this step:
Complete Domain Model Implementation: Full implementation of all entities, value objects, and aggregates for each capability
Domain Service Implementations: All domain services with pure domain logic
Specification Implementations: All business rules implemented as specifications
Domain Event Definitions: All domain events as immutable value objects
Factory Implementations: Factory methods and classes for complex object creation
Invariant Documentation: Clear documentation of all aggregate invariants and how they are enforced
STEP SIX: IMPLEMENTING THE REALIZATION LAYER WITH REPOSITORIES, APPLICATION SERVICES, AND INFRASTRUCTURE
With the domain model implemented in the Essence, we now implement the Realization layer that integrates with technical infrastructure using DDD patterns like Repositories, Application Services, and Infrastructure Services.
Rationale for this step: The Realization layer is where we handle all infrastructure concerns while preserving the purity of the domain model. By implementing Repositories, we provide collection-like access to aggregates while hiding persistence details. Application Services orchestrate use cases, manage transactions, and coordinate between domain logic and infrastructure. This separation allows us to change infrastructure technologies without affecting domain logic.
How to execute this step: For each capability, implement the Realization layer by identifying infrastructure needs and applying the appropriate DDD patterns:
Repositories: Create repository interfaces in the Essence layer (as they are part of the domain model's conceptual surface) and implementations in the Realization layer. Repositories should provide collection-like access to aggregates, hiding all persistence details.
Application Services: Create application services that orchestrate use cases. These services coordinate between domain objects, repositories, and infrastructure services. They manage transactions and publish domain events.
Infrastructure Services: Create services for technical concerns like email sending, file storage, or external API calls. These are distinct from domain services.
Let us implement the complete Realization layer for the Production Planning capability:
// ===================================================================
// PRODUCTION PLANNING CAPABILITY - REALIZATION LAYER
// Repositories, Application Services, and Infrastructure
// ===================================================================
/**
* REPOSITORY INTERFACE: ProductionOrderRepository
*
* The repository interface is part of the domain model's conceptual
* surface, so it's defined in the Essence layer. However, the
* implementation is in the Realization layer because it requires
* infrastructure.
*
* This interface provides collection-like access to ProductionOrder
* aggregates, hiding all persistence details.
*/
public interface ProductionOrderRepository {
/**
* Find a production order by ID.
* Returns Optional because the order might not exist.
*/
Optional<ProductionOrder> findById(ProductionOrderId id);
/**
* Find all production orders.
* Repository provides collection-like interface.
*/
List<ProductionOrder> findAll();
/**
* Find production orders by specification.
* This combines the Repository and Specification patterns.
*/
List<ProductionOrder> findBySpecification(
ProductionOrderSpecification specification
);
/**
* Find production orders by status.
* Common query method.
*/
List<ProductionOrder> findByStatus(ProductionOrderStatus status);
/**
* Find production orders scheduled between dates.
*/
List<ProductionOrder> findScheduledBetween(
Instant startTime,
Instant endTime
);
/**
* Save a production order aggregate.
* Repository handles persistence of the entire aggregate,
* including all entities and value objects within it.
*/
void save(ProductionOrder order);
/**
* Delete a production order aggregate.
*/
void delete(ProductionOrderId id);
/**
* Get the next available order ID.
* Some repositories provide ID generation.
*/
ProductionOrderId nextId();
}
/**
* REPOSITORY IMPLEMENTATION: JpaProductionOrderRepository
*
* Concrete implementation using JPA for persistence.
* This is in the Realization layer.
*/
public class JpaProductionOrderRepository
implements ProductionOrderRepository {
private final EntityManager entityManager;
public JpaProductionOrderRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Optional<ProductionOrder> findById(ProductionOrderId id) {
ProductionOrderJpaEntity entity = entityManager.find(
ProductionOrderJpaEntity.class,
id.getValue()
);
if (entity == null) {
return Optional.empty();
}
return Optional.of(toDomainModel(entity));
}
@Override
public List<ProductionOrder> findAll() {
List<ProductionOrderJpaEntity> entities = entityManager
.createQuery(
"SELECT po FROM ProductionOrderJpaEntity po",
ProductionOrderJpaEntity.class
)
.getResultList();
return entities.stream()
.map(this::toDomainModel)
.collect(Collectors.toList());
}
@Override
public List<ProductionOrder> findBySpecification(
ProductionOrderSpecification specification
) {
// Load all orders and filter using specification
// In a real implementation, you might translate the specification
// to a database query for better performance
return findAll().stream()
.filter(specification::isSatisfiedBy)
.collect(Collectors.toList());
}
@Override
public List<ProductionOrder> findByStatus(
ProductionOrderStatus status
) {
List<ProductionOrderJpaEntity> entities = entityManager
.createQuery(
"SELECT po FROM ProductionOrderJpaEntity po " +
"WHERE po.status = :status",
ProductionOrderJpaEntity.class
)
.setParameter("status", status.name())
.getResultList();
return entities.stream()
.map(this::toDomainModel)
.collect(Collectors.toList());
}
@Override
public List<ProductionOrder> findScheduledBetween(
Instant startTime,
Instant endTime
) {
List<ProductionOrderJpaEntity> entities = entityManager
.createQuery(
"SELECT po FROM ProductionOrderJpaEntity po " +
"WHERE po.scheduledStartTime >= :start " +
"AND po.scheduledEndTime <= :end",
ProductionOrderJpaEntity.class
)
.setParameter("start", startTime)
.setParameter("end", endTime)
.getResultList();
return entities.stream()
.map(this::toDomainModel)
.collect(Collectors.toList());
}
@Override
public void save(ProductionOrder order) {
ProductionOrderJpaEntity entity = toJpaEntity(order);
ProductionOrderJpaEntity existing = entityManager.find(
ProductionOrderJpaEntity.class,
order.getId().getValue()
);
if (existing == null) {
entityManager.persist(entity);
} else {
entityManager.merge(entity);
}
}
@Override
public void delete(ProductionOrderId id) {
ProductionOrderJpaEntity entity = entityManager.find(
ProductionOrderJpaEntity.class,
id.getValue()
);
if (entity != null) {
entityManager.remove(entity);
}
}
@Override
public ProductionOrderId nextId() {
return ProductionOrderId.generate();
}
/**
* TRANSLATION: Domain Model to JPA Entity
*
* This is the repository's responsibility - translating between
* the rich domain model and the persistence model.
*/
private ProductionOrderJpaEntity toJpaEntity(ProductionOrder order) {
ProductionOrderJpaEntity entity = new ProductionOrderJpaEntity();
entity.setId(order.getId().getValue());
entity.setProductCode(order.getProductCode().getValue());
entity.setTargetQuantity(order.getTargetQuantity().getAmount());
entity.setTargetQuantityUnit(order.getTargetQuantity().getUnit());
entity.setPriority(order.getPriority().name());
entity.setStatus(order.getStatus().name());
if (order.getSchedule() != null) {
entity.setScheduledStartTime(order.getSchedule().getStartTime());
entity.setScheduledEndTime(order.getSchedule().getEndTime());
}
// Save order lines
List<ProductionOrderLineJpaEntity> lineEntities =
new ArrayList<>();
for (ProductionOrderLine line : order.getOrderLines()) {
ProductionOrderLineJpaEntity lineEntity =
new ProductionOrderLineJpaEntity();
lineEntity.setId(line.getId().getValue());
lineEntity.setOrderId(order.getId().getValue());
lineEntity.setStepName(line.getStep().getName());
lineEntity.setEstimatedDurationMinutes(
line.getEstimatedDuration().toMinutes()
);
lineEntity.setProducedQuantity(
line.getProducedQuantity().getAmount()
);
lineEntity.setComplete(line.isComplete());
lineEntities.add(lineEntity);
}
entity.setOrderLines(lineEntities);
return entity;
}
/**
* TRANSLATION: JPA Entity to Domain Model
*
* Reconstruct the rich domain model from the persistence model.
*/
private ProductionOrder toDomainModel(
ProductionOrderJpaEntity entity
) {
// Reconstruct the aggregate using the factory method
ProductionOrder order = ProductionOrder.create(
new ProductionOrderId(entity.getId()),
new ProductCode(entity.getProductCode()),
new Quantity(
entity.getTargetQuantity(),
entity.getTargetQuantityUnit()
),
ProductionPriority.valueOf(entity.getPriority()),
entity.getCreatedAt()
);
// Reconstruct order lines
for (ProductionOrderLineJpaEntity lineEntity :
entity.getOrderLines()) {
ProductionStep step = new ProductionStep(
lineEntity.getStepName()
);
Duration duration = Duration.ofMinutes(
lineEntity.getEstimatedDurationMinutes()
);
// In real implementation, we'd load resource requirements
List<ResourceRequirement> requirements = new ArrayList<>();
order.addProductionStep(step, duration, requirements);
// If the line is complete, mark it as such
if (lineEntity.isComplete()) {
ProductionOrderLineId lineId =
new ProductionOrderLineId(lineEntity.getId());
Quantity producedQty = new Quantity(
lineEntity.getProducedQuantity(),
entity.getTargetQuantityUnit()
);
order.completeStep(lineId, producedQty);
}
}
// Clear domain events that were raised during reconstruction
// We only want events from actual domain operations
order.getDomainEvents();
return order;
}
}
/**
* APPLICATION SERVICE: ProductionPlanningApplicationService
*
* Orchestrates use cases for production planning.
* Application services are in the Realization layer.
*
* Responsibilities:
* - Coordinate between domain objects and repositories
* - Manage transactions
* - Publish domain events
* - Interact with other capabilities through contracts
*/
public class ProductionPlanningApplicationService {
private final ProductionOrderRepository orderRepository;
private final ProductionSchedulingService schedulingService;
private final DomainEventPublisher eventPublisher;
// Contracts from other capabilities (injected by registry)
private MachineStatusContract.Provision machineStatus;
private MaintenanceScheduleContract.Provision maintenanceSchedule;
private MaterialAvailabilityContract.Provision materialAvailability;
public ProductionPlanningApplicationService(
ProductionOrderRepository orderRepository,
ProductionSchedulingService schedulingService,
DomainEventPublisher eventPublisher
) {
this.orderRepository = orderRepository;
this.schedulingService = schedulingService;
this.eventPublisher = eventPublisher;
}
/**
* Inject required contracts.
* Called by the capability registry.
*/
public void setMachineStatus(
MachineStatusContract.Provision machineStatus
) {
this.machineStatus = machineStatus;
}
public void setMaintenanceSchedule(
MaintenanceScheduleContract.Provision maintenanceSchedule
) {
this.maintenanceSchedule = maintenanceSchedule;
}
public void setMaterialAvailability(
MaterialAvailabilityContract.Provision materialAvailability
) {
this.materialAvailability = materialAvailability;
}
/**
* USE CASE: Create a new production order.
*
* This is an application service method that orchestrates
* the use case by coordinating domain objects and infrastructure.
*/
@Transactional
public ProductionOrderId createProductionOrder(
String productCode,
double quantity,
String unit,
ProductionPriority priority,
LocalDateTime requestedCompletionDate
) {
// Generate ID using repository
ProductionOrderId orderId = orderRepository.nextId();
// Create domain objects
ProductCode product = new ProductCode(productCode);
Quantity targetQuantity = new Quantity(quantity, unit);
// Use factory method to create aggregate
ProductionOrder order = ProductionOrder.create(
orderId,
product,
targetQuantity,
priority,
requestedCompletionDate
);
// Save aggregate using repository
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
return orderId;
}
/**
* USE CASE: Add production steps to an order.
*/
@Transactional
public void addProductionSteps(
String orderId,
List<ProductionStepDefinition> steps
) {
// Load aggregate from repository
ProductionOrder order = orderRepository
.findById(new ProductionOrderId(orderId))
.orElseThrow(() ->
new ProductionOrderNotFoundException(orderId)
);
// Execute domain logic for each step
for (ProductionStepDefinition stepDef : steps) {
ProductionStep step = new ProductionStep(stepDef.getName());
Duration duration = Duration.ofMinutes(stepDef.getDurationMinutes());
List<ResourceRequirement> requirements =
stepDef.getResourceRequirements().stream()
.map(this::toResourceRequirement)
.collect(Collectors.toList());
order.addProductionStep(step, duration, requirements);
}
// Save aggregate
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
}
/**
* USE CASE: Schedule a production order.
*
* This use case demonstrates interaction with other capabilities
* through contracts.
*/
@Transactional
public void scheduleProductionOrder(String orderId) {
// Load aggregate
ProductionOrder order = orderRepository
.findById(new ProductionOrderId(orderId))
.orElseThrow(() ->
new ProductionOrderNotFoundException(orderId)
);
// Gather machine availability from Machine Control capability
List<MachineAvailability> machineAvailability =
gatherMachineAvailability();
// Gather material availability from Material Management capability
List<MaterialAvailability> materialAvailabilityList =
gatherMaterialAvailability(order);
// Execute domain logic (schedule the order)
// This uses the domain service
order.schedule(
schedulingService,
machineAvailability,
materialAvailabilityList
);
// If scheduling succeeded, reserve materials
if (order.getStatus() == ProductionOrderStatus.SCHEDULED) {
reserveMaterials(order);
}
// Save aggregate
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
}
/**
* USE CASE: Start production for an order.
*/
@Transactional
public void startProduction(String orderId) {
ProductionOrder order = orderRepository
.findById(new ProductionOrderId(orderId))
.orElseThrow(() ->
new ProductionOrderNotFoundException(orderId)
);
// Execute domain logic
order.startProduction();
// Save aggregate
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
}
/**
* USE CASE: Complete a production step.
*/
@Transactional
public void completeProductionStep(
String orderId,
String lineId,
double producedQuantity,
String unit
) {
ProductionOrder order = orderRepository
.findById(new ProductionOrderId(orderId))
.orElseThrow(() ->
new ProductionOrderNotFoundException(orderId)
);
ProductionOrderLineId orderLineId =
new ProductionOrderLineId(lineId);
Quantity quantity = new Quantity(producedQuantity, unit);
// Execute domain logic
order.completeStep(orderLineId, quantity);
// Save aggregate
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
}
/**
* QUERY: Get production order details.
*
* Query methods don't modify state, just return data.
*/
public ProductionOrderDetails getProductionOrderDetails(
String orderId
) {
ProductionOrder order = orderRepository
.findById(new ProductionOrderId(orderId))
.orElseThrow(() ->
new ProductionOrderNotFoundException(orderId)
);
return ProductionOrderDetails.fromDomainModel(order);
}
/**
* QUERY: Get all production orders with a specific status.
*/
public List<ProductionOrderSummary> getOrdersByStatus(
ProductionOrderStatus status
) {
List<ProductionOrder> orders =
orderRepository.findByStatus(status);
return orders.stream()
.map(ProductionOrderSummary::fromDomainModel)
.collect(Collectors.toList());
}
/**
* QUERY: Get production schedule for a time period.
*/
public List<ScheduledOrderInfo> getProductionSchedule(
LocalDateTime startDate,
LocalDateTime endDate
) {
Instant start = startDate.atZone(ZoneId.systemDefault()).toInstant();
Instant end = endDate.atZone(ZoneId.systemDefault()).toInstant();
List<ProductionOrder> orders =
orderRepository.findScheduledBetween(start, end);
return orders.stream()
.map(ScheduledOrderInfo::fromDomainModel)
.collect(Collectors.toList());
}
/**
* Helper: Gather machine availability from Machine Control capability.
* This demonstrates cross-capability interaction through contracts.
*/
private List<MachineAvailability> gatherMachineAvailability() {
List<MachineAvailability> availability = new ArrayList<>();
// Get all machine statuses from Machine Control capability
List<MachineStatusContract.MachineStatus> machineStatuses =
machineStatus.getAllMachineStatus();
for (MachineStatusContract.MachineStatus status : machineStatuses) {
// Check maintenance schedule from Equipment Health capability
boolean hasScheduledMaintenance =
maintenanceSchedule.isMachineAvailable(
status.getMachineId(),
Instant.now(),
Instant.now().plus(Duration.ofDays(7))
);
if (status.isAvailable() && hasScheduledMaintenance) {
// Get availability forecast
MachineStatusContract.AvailabilityForecast forecast =
machineStatus.getAvailabilityForecast(
status.getMachineId(),
Duration.ofDays(7)
);
// Translate contract DTO to domain model
availability.add(new MachineAvailability(
status.getMachineId(),
forecast.getAvailableFrom(),
forecast.getAvailableUntil(),
translateCapabilities(status.getCapabilities())
));
}
}
return availability;
}
/**
* Helper: Gather material availability from Material Management capability.
*/
private List<MaterialAvailability> gatherMaterialAvailability(
ProductionOrder order
) {
List<MaterialAvailability> availability = new ArrayList<>();
// Collect all material requirements from the order
for (ProductionOrderLine line : order.getOrderLines()) {
for (ResourceRequirement requirement :
line.getResourceRequirements()) {
// Check availability through Material Management contract
MaterialAvailabilityContract.MaterialAvailabilityStatus status =
materialAvailability.checkAvailability(
toContractRequirement(requirement)
);
if (status.isAvailable()) {
availability.add(new MaterialAvailability(
requirement.getResourceType(),
new Quantity(
status.getAvailableQuantity(),
status.getUnit()
),
status.getAvailableFrom()
));
}
}
}
return availability;
}
/**
* Helper: Reserve materials for a scheduled order.
*/
private void reserveMaterials(ProductionOrder order) {
List<MaterialAvailabilityContract.MaterialRequirement> requirements =
new ArrayList<>();
for (ProductionOrderLine line : order.getOrderLines()) {
for (ResourceRequirement requirement :
line.getResourceRequirements()) {
requirements.add(toContractRequirement(requirement));
}
}
// Reserve materials through Material Management contract
MaterialAvailabilityContract.ReservationResult result =
materialAvailability.reserveMaterials(
order.getId().getValue(),
requirements
);
if (!result.isSuccessful()) {
throw new MaterialReservationFailedException(
"Failed to reserve materials: " + result.getErrorMessage()
);
}
}
/**
* Helper: Publish domain events.
*/
private void publishDomainEvents(ProductionOrder order) {
List<DomainEvent> events = order.getDomainEvents();
for (DomainEvent event : events) {
eventPublisher.publish(event);
}
}
/**
* Helper: Translate DTO to domain model.
*/
private ResourceRequirement toResourceRequirement(
ResourceRequirementDTO dto
) {
return new ResourceRequirement(
dto.getResourceType(),
new Quantity(dto.getQuantity(), dto.getUnit())
);
}
/**
* Helper: Translate domain model to contract DTO.
*/
private MaterialAvailabilityContract.MaterialRequirement
toContractRequirement(ResourceRequirement requirement) {
return new MaterialAvailabilityContract.MaterialRequirement(
requirement.getResourceType(),
requirement.getQuantity().getAmount(),
requirement.getQuantity().getUnit(),
LocalDate.now()
);
}
private List<ProductionCapability> translateCapabilities(
List<String> capabilities
) {
// Translate contract capabilities to domain model
return capabilities.stream()
.map(ProductionCapability::fromString)
.collect(Collectors.toList());
}
}
/**
* INFRASTRUCTURE SERVICE: DomainEventPublisher
*
* Infrastructure service for publishing domain events.
* This is distinct from domain services - it's a technical service.
*/
public interface DomainEventPublisher {
/**
* Publish a domain event to the event bus.
*/
void publish(DomainEvent event);
/**
* Publish multiple domain events.
*/
default void publishAll(List<DomainEvent> events) {
for (DomainEvent event : events) {
publish(event);
}
}
}
/**
* INFRASTRUCTURE SERVICE IMPLEMENTATION: MessageBrokerEventPublisher
*
* Concrete implementation using a message broker.
*/
public class MessageBrokerEventPublisher implements DomainEventPublisher {
private final MessageBroker messageBroker;
private final EventSerializer eventSerializer;
public MessageBrokerEventPublisher(
MessageBroker messageBroker,
EventSerializer eventSerializer
) {
this.messageBroker = messageBroker;
this.eventSerializer = eventSerializer;
}
@Override
public void publish(DomainEvent event) {
// Determine the topic based on event type
String topic = determineTopicForEvent(event);
// Serialize the event
byte[] serializedEvent = eventSerializer.serialize(event);
// Publish to message broker
messageBroker.publish(topic, serializedEvent);
// Log the event publication
logEventPublication(event, topic);
}
private String determineTopicForEvent(DomainEvent event) {
// Map event types to topics
if (event instanceof ProductionOrderCreatedEvent) {
return "production.order.created";
} else if (event instanceof ProductionOrderScheduledEvent) {
return "production.order.scheduled";
} else if (event instanceof ProductionStartedEvent) {
return "production.started";
} else if (event instanceof ProductionOrderCompletedEvent) {
return "production.order.completed";
}
return "production.domain.event";
}
private void logEventPublication(DomainEvent event, String topic) {
System.out.println(
"Published domain event: " + event.getClass().getSimpleName() +
" to topic: " + topic +
" at: " + event.occurredAt()
);
}
}
/**
* FACTORY: ProductionOrderFactory
*
* Complex factory that requires infrastructure to create production orders.
* This is in the Realization layer because it needs repositories and
* external services.
*/
public class ProductionOrderFactory {
private final ProductionOrderRepository orderRepository;
private final ProductCatalogService productCatalog;
private final ProductionTemplateRepository templateRepository;
public ProductionOrderFactory(
ProductionOrderRepository orderRepository,
ProductCatalogService productCatalog,
ProductionTemplateRepository templateRepository
) {
this.orderRepository = orderRepository;
this.productCatalog = productCatalog;
this.templateRepository = templateRepository;
}
/**
* Create a production order from a template.
* This is complex construction that requires infrastructure.
*/
public ProductionOrder createFromTemplate(
String templateId,
Quantity targetQuantity,
ProductionPriority priority
) {
// Load template from repository
ProductionTemplate template = templateRepository
.findById(templateId)
.orElseThrow(() ->
new TemplateNotFoundException(templateId)
);
// Get product information from catalog service
ProductInfo productInfo = productCatalog.getProductInfo(
template.getProductCode()
);
// Generate order ID
ProductionOrderId orderId = orderRepository.nextId();
// Create the order
ProductionOrder order = ProductionOrder.create(
orderId,
new ProductCode(template.getProductCode()),
targetQuantity,
priority,
LocalDateTime.now().plusDays(template.getStandardLeadTimeDays())
);
// Add production steps from template
for (ProductionStepTemplate stepTemplate :
template.getSteps()) {
ProductionStep step = new ProductionStep(
stepTemplate.getName()
);
Duration duration = calculateDuration(
stepTemplate,
targetQuantity,
productInfo
);
List<ResourceRequirement> requirements =
calculateResourceRequirements(
stepTemplate,
targetQuantity,
productInfo
);
order.addProductionStep(step, duration, requirements);
}
return order;
}
private Duration calculateDuration(
ProductionStepTemplate stepTemplate,
Quantity targetQuantity,
ProductInfo productInfo
) {
// Complex calculation based on template, quantity, and product info
double baseMinutes = stepTemplate.getBaseMinutesPerUnit();
double setupMinutes = stepTemplate.getSetupMinutes();
double totalMinutes = setupMinutes +
(baseMinutes * targetQuantity.getAmount());
return Duration.ofMinutes((long) totalMinutes);
}
private List<ResourceRequirement> calculateResourceRequirements(
ProductionStepTemplate stepTemplate,
Quantity targetQuantity,
ProductInfo productInfo
) {
// Calculate resource requirements based on template and quantity
List<ResourceRequirement> requirements = new ArrayList<>();
for (ResourceTemplate resourceTemplate :
stepTemplate.getResources()) {
double requiredQuantity =
resourceTemplate.getQuantityPerUnit() *
targetQuantity.getAmount();
requirements.add(new ResourceRequirement(
resourceTemplate.getResourceType(),
new Quantity(
requiredQuantity,
resourceTemplate.getUnit()
)
));
}
return requirements;
}
}
Content created in this step:
Repository Interfaces: Defined in Essence layer as part of domain model's conceptual surface
Repository Implementations: Complete implementations in Realization layer with:
- Persistence logic using JPA or other technologies
- Translation between domain models and persistence models
- Query implementations
Application Services: Complete orchestration of use cases including:
- Transaction management
- Coordination between domain objects and repositories
- Domain event publishing
- Cross-capability interaction through contracts
Infrastructure Services: Technical services like event publishers, email senders, etc.
Complex Factories: Factories that require infrastructure to create domain objects
Translation Logic: Comprehensive translation between domain models, persistence models, and contract DTOs
STEP SEVEN: IMPLEMENTING THE ADAPTATION LAYER WITH ANTICORRUPTION LAYERS AND CONTRACT IMPLEMENTATIONS
With both Essence and Realization implemented, we now create the Adaptation layer that provides external interfaces, implements contracts, and includes Anticorruption Layers where needed.
Rationale for this step: The Adaptation layer is the boundary between the capability and the outside world. By implementing contracts in the Adaptation layer, we create stable interfaces that other capabilities can depend on. Anticorruption Layers protect our domain model from external models that would corrupt it. This layer is critical for maintaining the integrity of our bounded context.
How to execute this step: For each capability, implement the Adaptation layer by:
- Implementing provided contracts (what this capability offers to others)
- Consuming required contracts (what this capability needs from others)
- Creating Anticorruption Layers for external systems
- Implementing event publishers and subscribers
- Providing REST APIs or other external interfaces
Let us implement the complete Adaptation layer for the Production Planning capability:
// ===================================================================
// PRODUCTION PLANNING CAPABILITY - ADAPTATION LAYER
// Contract Implementations, ACLs, and External Interfaces
// ===================================================================
/**
* CONTRACT IMPLEMENTATION: ProductionBatchContract
*
* This implements the contract that Production Planning provides
* to other capabilities (like Quality Assurance).
*
* This is an Open Host Service pattern - designed to serve
* multiple different consumers.
*/
public class ProductionBatchContractImpl
implements ProductionBatchContract.Provision {
private final ProductionPlanningApplicationService applicationService;
private final ProductionOrderRepository orderRepository;
public ProductionBatchContractImpl(
ProductionPlanningApplicationService applicationService,
ProductionOrderRepository orderRepository
) {
this.applicationService = applicationService;
this.orderRepository = orderRepository;
}
@Override
public BatchInfo getBatchInfo(String batchId) {
// In this domain, a batch is a production order
ProductionOrderDetails details =
applicationService.getProductionOrderDetails(batchId);
// Translate domain model to contract DTO
return new BatchInfo(
details.getOrderId(),
details.getProductCode(),
details.getTargetQuantity(),
details.getStatus().name(),
details.getScheduledStart(),
details.getScheduledEnd()
);
}
@Override
public List<BatchInfo> getActiveBatches() {
List<ProductionOrderSummary> activeOrders =
applicationService.getOrdersByStatus(
ProductionOrderStatus.IN_PROGRESS
);
return activeOrders.stream()
.map(this::toBatchInfo)
.collect(Collectors.toList());
}
@Override
public List<BatchInfo> getBatchesScheduledBetween(
LocalDateTime startDate,
LocalDateTime endDate
) {
List<ScheduledOrderInfo> scheduledOrders =
applicationService.getProductionSchedule(startDate, endDate);
return scheduledOrders.stream()
.map(this::toBatchInfo)
.collect(Collectors.toList());
}
@Override
public void subscribeToBatchEvents(BatchEventSubscriber subscriber) {
// Register subscriber to receive batch events
// Implementation would use the domain event publisher
// to notify subscribers when production events occur
}
/**
* Translate domain model to contract DTO.
* This is the Adaptation layer's responsibility.
*/
private BatchInfo toBatchInfo(ProductionOrderSummary summary) {
return new BatchInfo(
summary.getOrderId(),
summary.getProductCode(),
summary.getTargetQuantity(),
summary.getStatus(),
summary.getScheduledStart(),
summary.getScheduledEnd()
);
}
private BatchInfo toBatchInfo(ScheduledOrderInfo info) {
return new BatchInfo(
info.getOrderId(),
info.getProductCode(),
info.getTargetQuantity(),
"SCHEDULED",
info.getStartTime(),
info.getEndTime()
);
}
}
/**
* CONTRACT IMPLEMENTATION: MaterialRequirementContract
*
* This implements the Partnership pattern contract.
* Production Planning provides material requirements to
* Material Management. Both must coordinate evolution.
*/
public class MaterialRequirementContractImpl
implements MaterialRequirementContract.Provision {
private final ProductionOrderRepository orderRepository;
private final MaterialRequirementCalculator requirementCalculator;
public MaterialRequirementContractImpl(
ProductionOrderRepository orderRepository,
MaterialRequirementCalculator requirementCalculator
) {
this.orderRepository = orderRepository;
this.requirementCalculator = requirementCalculator;
}
@Override
public void submitRequirements(
String productionOrderId,
List<MaterialRequirement> requirements
) {
// This is called by Material Management to confirm
// they've received our requirements
// We could store this confirmation in the domain model
// or just log it
System.out.println(
"Material requirements confirmed for order: " +
productionOrderId
);
}
@Override
public void updateRequirements(
String productionOrderId,
List<MaterialRequirement> updatedRequirements
) {
// Handle updates to material requirements
// This might trigger rescheduling
}
@Override
public List<MaterialForecast> getForecastedRequirements(
Duration forecastPeriod
) {
// Get all scheduled orders in the forecast period
LocalDateTime endDate = LocalDateTime.now().plus(forecastPeriod);
List<ScheduledOrderInfo> scheduledOrders =
orderRepository.findScheduledBetween(
Instant.now(),
endDate.atZone(ZoneId.systemDefault()).toInstant()
).stream()
.map(ScheduledOrderInfo::fromDomainModel)
.collect(Collectors.toList());
// Calculate material requirements for each order
List<MaterialForecast> forecasts = new ArrayList<>();
for (ScheduledOrderInfo orderInfo : scheduledOrders) {
ProductionOrder order = orderRepository
.findById(new ProductionOrderId(orderInfo.getOrderId()))
.orElse(null);
if (order != null) {
List<MaterialRequirement> requirements =
requirementCalculator.calculateRequirements(order);
for (MaterialRequirement requirement : requirements) {
forecasts.add(new MaterialForecast(
requirement,
0.8, // 80% confidence
orderInfo.getStartTime().atZone(
ZoneId.systemDefault()
).toLocalDate()
));
}
}
}
return forecasts;
}
}
/**
* CONTRACT CONSUMER: MachineStatusContractConsumer
*
* This consumes the MachineStatusContract from Machine Control.
* This is a Conformist pattern - we use the contract as-is.
*/
public class MachineStatusContractConsumer {
private final ProductionPlanningApplicationService applicationService;
private MachineStatusContract.Provision machineStatus;
public MachineStatusContractConsumer(
ProductionPlanningApplicationService applicationService
) {
this.applicationService = applicationService;
}
/**
* Inject the required contract.
* Called by the capability registry.
*/
public void setMachineStatus(
MachineStatusContract.Provision machineStatus
) {
this.machineStatus = machineStatus;
// Pass the contract to the application service
applicationService.setMachineStatus(machineStatus);
}
/**
* Query machine availability for scheduling.
* This is a convenience method that wraps the contract.
*/
public boolean isMachineAvailable(
String machineId,
Instant startTime,
Duration duration
) {
MachineStatusContract.MachineStatus status =
machineStatus.getMachineStatus(machineId);
if (!status.isAvailable()) {
return false;
}
// Check availability forecast
MachineStatusContract.AvailabilityForecast forecast =
machineStatus.getAvailabilityForecast(machineId, duration);
return forecast.isAvailableDuring(startTime, startTime.plus(duration));
}
}
/**
* CONTRACT CONSUMER: MaintenanceScheduleContractConsumer
*
* Consumes the MaintenanceScheduleContract from Equipment Health.
* Also uses Conformist pattern.
*/
public class MaintenanceScheduleContractConsumer {
private final ProductionPlanningApplicationService applicationService;
private MaintenanceScheduleContract.Provision maintenanceSchedule;
public MaintenanceScheduleContractConsumer(
ProductionPlanningApplicationService applicationService
) {
this.applicationService = applicationService;
}
public void setMaintenanceSchedule(
MaintenanceScheduleContract.Provision maintenanceSchedule
) {
this.maintenanceSchedule = maintenanceSchedule;
applicationService.setMaintenanceSchedule(maintenanceSchedule);
}
/**
* Check if a machine has scheduled maintenance that would
* conflict with production.
*/
public boolean hasMaintenanceConflict(
String machineId,
Instant startTime,
Instant endTime
) {
return !maintenanceSchedule.isMachineAvailable(
machineId,
startTime,
endTime
);
}
/**
* Get all upcoming maintenance to factor into planning.
*/
public List<MaintenanceWindow> getUpcomingMaintenance(
Duration lookahead
) {
return maintenanceSchedule.getUpcomingMaintenance(lookahead);
}
}
/**
* CONTRACT CONSUMER WITH ACL: MaterialAvailabilityContractConsumer
*
* This consumes the MaterialAvailabilityContract from Material Management.
* This is a Partnership pattern, but we still translate to protect
* our domain model.
*/
public class MaterialAvailabilityContractConsumer {
private final ProductionPlanningApplicationService applicationService;
private MaterialAvailabilityContract.Provision materialAvailability;
public MaterialAvailabilityContractConsumer(
ProductionPlanningApplicationService applicationService
) {
this.applicationService = applicationService;
}
public void setMaterialAvailability(
MaterialAvailabilityContract.Provision materialAvailability
) {
this.materialAvailability = materialAvailability;
applicationService.setMaterialAvailability(materialAvailability);
}
/**
* Check material availability and translate to domain model.
* This provides some protection even in a partnership.
*/
public boolean areMaterialsAvailable(
List<ResourceRequirement> requirements
) {
for (ResourceRequirement requirement : requirements) {
MaterialAvailabilityContract.MaterialRequirement contractReq =
new MaterialAvailabilityContract.MaterialRequirement(
requirement.getResourceType(),
requirement.getQuantity().getAmount(),
requirement.getQuantity().getUnit(),
LocalDate.now()
);
MaterialAvailabilityContract.MaterialAvailabilityStatus status =
materialAvailability.checkAvailability(contractReq);
if (!status.isAvailable()) {
return false;
}
}
return true;
}
}
/**
* ANTICORRUPTION LAYER: LegacyERPAnticorruptionLayer
*
* This is a complete Anticorruption Layer for integrating with
* a legacy ERP system. The legacy system has a poor model that
* we must protect our domain from.
*
* This demonstrates the Anticorruption Layer pattern from DDD.
*/
public class LegacyERPAnticorruptionLayer {
private final LegacyERPContract.Provision legacyERP;
private final ERPProductCodeMapper productCodeMapper;
private final ERPStatusTranslator statusTranslator;
public LegacyERPAnticorruptionLayer(
LegacyERPContract.Provision legacyERP,
ERPProductCodeMapper productCodeMapper,
ERPStatusTranslator statusTranslator
) {
this.legacyERP = legacyERP;
this.productCodeMapper = productCodeMapper;
this.statusTranslator = statusTranslator;
}
/**
* Create a production order in the legacy ERP system.
* Translates from our clean domain model to the legacy format.
*/
public void createProductionOrderInERP(ProductionOrder order) {
// Translate our product code to legacy ERP product code
String legacyProductCode = productCodeMapper.toLegacyCode(
order.getProductCode().getValue()
);
// Legacy ERP uses a flat structure with cryptic field names
LegacyERPContract.ProductionOrderData legacyData =
new LegacyERPContract.ProductionOrderData();
legacyData.ordNum = order.getId().getValue();
legacyData.prdCd = legacyProductCode;
legacyData.qty = order.getTargetQuantity().getAmount();
legacyData.uom = translateUnit(order.getTargetQuantity().getUnit());
legacyData.pri = translatePriority(order.getPriority());
legacyData.sts = "10"; // Legacy status code for "Created"
// Legacy ERP expects dates as strings in MM/DD/YYYY format
if (order.getSchedule() != null) {
legacyData.stDt = formatLegacyDate(
order.getSchedule().getStartTime()
);
legacyData.endDt = formatLegacyDate(
order.getSchedule().getEndTime()
);
}
// Call legacy ERP
try {
legacyERP.createOrder(legacyData);
} catch (LegacyERPException e) {
throw new ERPIntegrationException(
"Failed to create order in legacy ERP",
e
);
}
}
/**
* Update production order status in legacy ERP.
*/
public void updateOrderStatusInERP(
ProductionOrderId orderId,
ProductionOrderStatus status
) {
String legacyStatus = statusTranslator.toLegacyStatus(status);
try {
legacyERP.updateOrderStatus(
orderId.getValue(),
legacyStatus
);
} catch (LegacyERPException e) {
throw new ERPIntegrationException(
"Failed to update order status in legacy ERP",
e
);
}
}
/**
* Retrieve production order from legacy ERP and translate
* to our domain model.
*/
public ProductionOrderData retrieveOrderFromERP(String orderId) {
try {
LegacyERPContract.ProductionOrderData legacyData =
legacyERP.getOrder(orderId);
// Translate from legacy format to our domain model
return translateFromLegacy(legacyData);
} catch (LegacyERPException e) {
throw new ERPIntegrationException(
"Failed to retrieve order from legacy ERP",
e
);
}
}
/**
* Translate legacy ERP data to our domain model.
* This is the core of the Anticorruption Layer.
*/
private ProductionOrderData translateFromLegacy(
LegacyERPContract.ProductionOrderData legacyData
) {
// Translate product code from legacy to our system
String ourProductCode = productCodeMapper.fromLegacyCode(
legacyData.prdCd
);
// Translate status from legacy codes to our enum
ProductionOrderStatus status = statusTranslator.fromLegacyStatus(
legacyData.sts
);
// Translate unit of measure
String unit = translateUnitFromLegacy(legacyData.uom);
// Translate priority
ProductionPriority priority = translatePriorityFromLegacy(
legacyData.pri
);
// Parse legacy date format
LocalDateTime scheduledStart = parseLegacyDate(legacyData.stDt);
LocalDateTime scheduledEnd = parseLegacyDate(legacyData.endDt);
// Create our clean domain object
return new ProductionOrderData(
legacyData.ordNum,
ourProductCode,
legacyData.qty,
unit,
priority,
status,
scheduledStart,
scheduledEnd
);
}
/**
* Translate our unit to legacy unit code.
* Legacy system uses cryptic codes.
*/
private String translateUnit(String unit) {
switch (unit.toLowerCase()) {
case "pieces": return "PC";
case "kilograms": return "KG";
case "liters": return "LT";
case "meters": return "MT";
default: return "EA"; // Each
}
}
/**
* Translate legacy unit code to our unit.
*/
private String translateUnitFromLegacy(String legacyUnit) {
switch (legacyUnit) {
case "PC": return "pieces";
case "KG": return "kilograms";
case "LT": return "liters";
case "MT": return "meters";
case "EA": return "units";
default: return "units";
}
}
/**
* Translate our priority to legacy priority code.
*/
private String translatePriority(ProductionPriority priority) {
switch (priority) {
case URGENT: return "1";
case HIGH: return "2";
case NORMAL: return "3";
case LOW: return "4";
default: return "3";
}
}
/**
* Translate legacy priority code to our priority.
*/
private ProductionPriority translatePriorityFromLegacy(String legacyPri) {
switch (legacyPri) {
case "1": return ProductionPriority.URGENT;
case "2": return ProductionPriority.HIGH;
case "3": return ProductionPriority.NORMAL;
case "4": return ProductionPriority.LOW;
default: return ProductionPriority.NORMAL;
}
}
/**
* Format date for legacy ERP (MM/DD/YYYY format).
*/
private String formatLegacyDate(Instant instant) {
LocalDateTime dateTime = LocalDateTime.ofInstant(
instant,
ZoneId.systemDefault()
);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
"MM/dd/yyyy"
);
return dateTime.format(formatter);
}
/**
* Parse legacy ERP date format.
*/
private LocalDateTime parseLegacyDate(String legacyDate) {
if (legacyDate == null || legacyDate.trim().isEmpty()) {
return null;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
"MM/dd/yyyy"
);
LocalDate date = LocalDate.parse(legacyDate, formatter);
return date.atStartOfDay();
}
}
/**
* DOMAIN EVENT SUBSCRIBER: ProductionEventSubscriber
*
* Subscribes to domain events from other capabilities and
* translates them to actions in this capability.
*/
public class ProductionEventSubscriber {
private final ProductionPlanningApplicationService applicationService;
private final LegacyERPAnticorruptionLayer erpACL;
public ProductionEventSubscriber(
ProductionPlanningApplicationService applicationService,
LegacyERPAnticorruptionLayer erpACL
) {
this.applicationService = applicationService;
this.erpACL = erpACL;
}
/**
* Subscribe to domain events.
* Called during capability initialization.
*/
public void subscribeToEvents(DomainEventBus eventBus) {
eventBus.subscribe(
ProductionOrderCreatedEvent.class,
this::handleProductionOrderCreated
);
eventBus.subscribe(
ProductionOrderScheduledEvent.class,
this::handleProductionOrderScheduled
);
eventBus.subscribe(
ProductionOrderCompletedEvent.class,
this::handleProductionOrderCompleted
);
}
/**
* Handle ProductionOrderCreated event.
* Sync to legacy ERP system.
*/
private void handleProductionOrderCreated(
ProductionOrderCreatedEvent event
) {
// Load the order
ProductionOrder order = applicationService
.getProductionOrderRepository()
.findById(event.getOrderId())
.orElse(null);
if (order != null) {
// Sync to legacy ERP through ACL
erpACL.createProductionOrderInERP(order);
}
}
/**
* Handle ProductionOrderScheduled event.
*/
private void handleProductionOrderScheduled(
ProductionOrderScheduledEvent event
) {
// Update status in legacy ERP
erpACL.updateOrderStatusInERP(
event.getOrderId(),
ProductionOrderStatus.SCHEDULED
);
}
/**
* Handle ProductionOrderCompleted event.
*/
private void handleProductionOrderCompleted(
ProductionOrderCompletedEvent event
) {
// Update status in legacy ERP
erpACL.updateOrderStatusInERP(
event.getOrderId(),
ProductionOrderStatus.COMPLETED
);
}
}
/**
* REST API ADAPTER: ProductionPlanningRestController
*
* Provides REST API for external systems to interact with
* Production Planning capability.
*
* This is another form of adaptation - adapting HTTP requests
* to domain operations.
*/
@RestController
@RequestMapping("/api/production")
public class ProductionPlanningRestController {
private final ProductionPlanningApplicationService applicationService;
public ProductionPlanningRestController(
ProductionPlanningApplicationService applicationService
) {
this.applicationService = applicationService;
}
/**
* Create a new production order.
*/
@PostMapping("/orders")
public ResponseEntity<CreateOrderResponse> createOrder(
@RequestBody CreateOrderRequest request
) {
try {
ProductionOrderId orderId =
applicationService.createProductionOrder(
request.getProductCode(),
request.getQuantity(),
request.getUnit(),
ProductionPriority.valueOf(request.getPriority()),
request.getRequestedCompletionDate()
);
return ResponseEntity.ok(
new CreateOrderResponse(orderId.getValue())
);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
/**
* Get production order details.
*/
@GetMapping("/orders/{orderId}")
public ResponseEntity<ProductionOrderResponse> getOrder(
@PathVariable String orderId
) {
try {
ProductionOrderDetails details =
applicationService.getProductionOrderDetails(orderId);
return ResponseEntity.ok(
ProductionOrderResponse.fromDomainModel(details)
);
} catch (ProductionOrderNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
/**
* Schedule a production order.
*/
@PostMapping("/orders/{orderId}/schedule")
public ResponseEntity<Void> scheduleOrder(
@PathVariable String orderId
) {
try {
applicationService.scheduleProductionOrder(orderId);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
/**
* Get production schedule.
*/
@GetMapping("/schedule")
public ResponseEntity<List<ScheduleEntryResponse>> getSchedule(
@RequestParam LocalDateTime startDate,
@RequestParam LocalDateTime endDate
) {
List<ScheduledOrderInfo> schedule =
applicationService.getProductionSchedule(startDate, endDate);
List<ScheduleEntryResponse> response = schedule.stream()
.map(ScheduleEntryResponse::fromDomainModel)
.collect(Collectors.toList());
return ResponseEntity.ok(response);
}
}
Content created in this step:
Contract Provision Implementations: Complete implementations of all contracts this capability provides
Contract Requirement Consumers: Wrappers for consuming contracts from other capabilities
Anticorruption Layer Implementations: Complete ACLs for external systems with:
- Translation from external models to domain models
- Translation from domain models to external models
- Protection of domain model integrity
Domain Event Subscribers: Handlers for domain events from other capabilities
External Interface Adapters: REST APIs, message queue consumers, etc.
Translation Logic: Comprehensive translation between:
- Domain models and contract DTOs
- Domain models and external system formats
- Domain models and API request/response formats
STEP EIGHT: ASSEMBLING CAPABILITIES WITH COMPLETE DEPENDENCY MANAGEMENT
With all capabilities implemented, we now assemble them into a complete system by registering them, establishing contract bindings, and managing the dependency graph to prevent circular dependencies.
Rationale for this step: Individual capabilities are useless in isolation. They must be connected through their contracts to form a functioning system. The capability registry manages these connections, validates the dependency graph against the context map, ensures there are no circular dependencies, and establishes the correct initialization order based on the topological sort of the dependency graph.
How to execute this step: Create a comprehensive capability registry that validates the system against the context map, enforces relationship patterns, and manages the complete lifecycle.
// ===================================================================
// SYSTEM ASSEMBLY: Complete Capability Registry and Bootstrap
// ===================================================================
/**
* CAPABILITY REGISTRY: Enhanced with DDD Context Map Validation
*
* This registry not only manages capabilities and contracts,
* but also validates that the implementation matches the
* strategic design from the context map.
*/
public class EnhancedCapabilityRegistry extends CapabilityRegistry {
private final ContextMap contextMap;
private final Map<String, BoundedContextDescriptor> contexts;
public EnhancedCapabilityRegistry(ContextMap contextMap) {
super();
this.contextMap = contextMap;
this.contexts = new HashMap<>();
}
/**
* Register a capability with its bounded context.
* Validates that the capability matches its bounded context.
*/
public void registerCapabilityWithContext(
CapabilityDescriptor capability,
BoundedContextDescriptor context
) {
// Validate that capability implements its bounded context correctly
validateCapabilityMatchesContext(capability, context);
// Store context information
contexts.put(capability.getName(), context);
// Register the capability
super.registerCapability(capability);
}
/**
* Bind a contract with relationship pattern validation.
* Ensures the binding matches the context map.
*/
public void bindContractWithPattern(
String consumerName,
String providerName,
Class<?> contractType,
ContextRelationshipPattern expectedPattern
) {
// Validate that this binding matches the context map
ContextRelationship relationship = contextMap.getRelationship(
consumerName,
providerName
);
if (relationship == null) {
throw new UnmappedRelationshipException(
"No relationship defined in context map between " +
consumerName + " and " + providerName
);
}
if (relationship.getPattern() != expectedPattern) {
throw new RelationshipPatternMismatchException(
"Context map specifies " + relationship.getPattern() +
" but binding uses " + expectedPattern
);
}
// Perform the binding
super.bindContract(consumerName, providerName, contractType);
// Record the relationship pattern
recordRelationshipPattern(
consumerName,
providerName,
contractType,
expectedPattern
);
}
/**
* Validate the entire system against the context map.
* This ensures implementation matches strategic design.
*/
public ValidationReport validateAgainstContextMap() {
ValidationReport report = new ValidationReport();
// Check that all bounded contexts have capabilities
for (BoundedContext context : contextMap.getBoundedContexts()) {
if (!hasCapabilityForContext(context)) {
report.addError(
"No capability implemented for bounded context: " +
context.getName()
);
}
}
// Check that all relationships are implemented
for (ContextRelationship relationship :
contextMap.getRelationships()) {
if (!hasBindingForRelationship(relationship)) {
report.addError(
"No contract binding for relationship: " +
relationship.getUpstream() + " -> " +
relationship.getDownstream()
);
}
}
// Check for unexpected relationships
for (ContractBinding binding : getAllBindings()) {
if (!contextMap.hasRelationship(
binding.getConsumer(),
binding.getProvider()
)) {
report.addWarning(
"Contract binding exists but not in context map: " +
binding.getConsumer() + " -> " +
binding.getProvider()
);
}
}
return report;
}
/**
* Initialize all capabilities in dependency order.
* Enhanced with context map awareness.
*/
@Override
public void initializeAll() {
// Validate against context map first
ValidationReport report = validateAgainstContextMap();
if (report.hasErrors()) {
throw new ContextMapViolationException(
"System does not match context map:\n" +
report.getErrorSummary()
);
}
if (report.hasWarnings()) {
System.out.println("Context map warnings:\n" +
report.getWarningSummary()
);
}
// Proceed with initialization
super.initializeAll();
}
private void validateCapabilityMatchesContext(
CapabilityDescriptor capability,
BoundedContextDescriptor context
) {
// Validate ubiquitous language
Set<String> capabilityTerms = capability.getUbiquitousLanguageTerms();
Set<String> contextTerms = context.getUbiquitousLanguageTerms();
if (!capabilityTerms.equals(contextTerms)) {
throw new UbiquitousLanguageMismatchException(
"Capability " + capability.getName() +
" does not match ubiquitous language of context " +
context.getName()
);
}
// Validate responsibilities
// In a real implementation, this would be more sophisticated
}
private boolean hasCapabilityForContext(BoundedContext context) {
return contexts.values().stream()
.anyMatch(ctx -> ctx.getName().equals(context.getName()));
}
private boolean hasBindingForRelationship(
ContextRelationship relationship
) {
return getAllBindings().stream()
.anyMatch(binding ->
binding.getConsumer().equals(relationship.getDownstream()) &&
binding.getProvider().equals(relationship.getUpstream())
);
}
private void recordRelationshipPattern(
String consumer,
String provider,
Class<?> contractType,
ContextRelationshipPattern pattern
) {
// Record for documentation and validation
System.out.println(
"Relationship: " + provider + " -> " + consumer +
" using pattern: " + pattern +
" via contract: " + contractType.getSimpleName()
);
}
}
/**
* COMPLETE SYSTEM BOOTSTRAP
*
* This bootstraps the entire manufacturing system,
* demonstrating the complete integration of DDD and CCA.
*/
public class ManufacturingSystemBootstrap {
public static void main(String[] args) {
// Create context map from strategic design
ContextMap contextMap = createContextMap();
// Create enhanced registry
EnhancedCapabilityRegistry registry =
new EnhancedCapabilityRegistry(contextMap);
// Register all capabilities with their bounded contexts
registerCapabilities(registry, contextMap);
// Establish all contract bindings with pattern validation
establishContractBindings(registry);
// Validate and initialize
registry.initializeAll();
System.out.println("Manufacturing system initialized successfully!");
}
/**
* Create the context map from strategic DDD design.
*/
private static ContextMap createContextMap() {
ContextMap map = new ContextMap();
// Define bounded contexts
BoundedContext machineControl = new BoundedContext(
"MachineControl",
"Real-time control of industrial machines",
Set.of("Machine", "Sensor", "Actuator", "ControlLoop", "Setpoint")
);
BoundedContext equipmentHealth = new BoundedContext(
"EquipmentHealthMonitoring",
"Track equipment condition and predict maintenance",
Set.of("Equipment", "HealthMetric", "MaintenanceSchedule",
"Prediction")
);
BoundedContext productionPlanning = new BoundedContext(
"ProductionPlanning",
"Plan and optimize production schedules",
Set.of("ProductionOrder", "Schedule", "Resource", "Batch")
);
BoundedContext qualityAssurance = new BoundedContext(
"QualityAssurance",
"Ensure product quality and compliance",
Set.of("QualityCheck", "Defect", "Specification", "Compliance")
);
BoundedContext materialManagement = new BoundedContext(
"MaterialManagement",
"Manage raw materials and supplies",
Set.of("Material", "Inventory", "Supplier", "Order")
);
map.addContext(machineControl);
map.addContext(equipmentHealth);
map.addContext(productionPlanning);
map.addContext(qualityAssurance);
map.addContext(materialManagement);
// Define relationships with patterns
map.addRelationship(new ContextRelationship(
"MachineControl", // Upstream (Supplier)
"EquipmentHealthMonitoring", // Downstream (Customer)
ContextRelationshipPattern.PUBLISHED_LANGUAGE,
"Machine Control publishes sensor data and events"
));
map.addRelationship(new ContextRelationship(
"MachineControl",
"ProductionPlanning",
ContextRelationshipPattern.OPEN_HOST_SERVICE,
"Production Planning queries machine status"
));
map.addRelationship(new ContextRelationship(
"EquipmentHealthMonitoring",
"ProductionPlanning",
ContextRelationshipPattern.CUSTOMER_SUPPLIER,
"Production Planning uses maintenance schedules"
));
map.addRelationship(new ContextRelationship(
"ProductionPlanning",
"QualityAssurance",
ContextRelationshipPattern.CUSTOMER_SUPPLIER,
"Quality Assurance checks production batches"
));
map.addRelationship(new ContextRelationship(
"ProductionPlanning",
"MaterialManagement",
ContextRelationshipPattern.PARTNERSHIP,
"Bidirectional material requirements and availability"
));
map.addRelationship(new ContextRelationship(
"MaterialManagement",
"ProductionPlanning",
ContextRelationshipPattern.PARTNERSHIP,
"Bidirectional material requirements and availability"
));
return map;
}
/**
* Register all capabilities with their bounded contexts.
*/
private static void registerCapabilities(
EnhancedCapabilityRegistry registry,
ContextMap contextMap
) {
// Machine Control Capability
registry.registerCapabilityWithContext(
CapabilityDescriptor.builder()
.name("MachineControl")
.implementationClass(MachineControlCapability.class)
.provides(MachineMonitoringContract.class)
.provides(MachineStatusContract.class)
.provides(QualityDataContract.class)
.requires(LegacySCADAContract.class)
.ubiquitousLanguage(Set.of(
"Machine", "Sensor", "Actuator",
"ControlLoop", "Setpoint"
))
.build(),
contextMap.getContext("MachineControl")
);
// Equipment Health Monitoring Capability
registry.registerCapabilityWithContext(
CapabilityDescriptor.builder()
.name("EquipmentHealthMonitoring")
.implementationClass(EquipmentHealthCapability.class)
.provides(MaintenanceScheduleContract.class)
.requires(MachineMonitoringContract.class)
.ubiquitousLanguage(Set.of(
"Equipment", "HealthMetric",
"MaintenanceSchedule", "Prediction"
))
.build(),
contextMap.getContext("EquipmentHealthMonitoring")
);
// Production Planning Capability
registry.registerCapabilityWithContext(
CapabilityDescriptor.builder()
.name("ProductionPlanning")
.implementationClass(ProductionPlanningCapability.class)
.provides(ProductionBatchContract.class)
.provides(MaterialRequirementContract.class)
.requires(MachineStatusContract.class)
.requires(MaintenanceScheduleContract.class)
.requires(MaterialAvailabilityContract.class)
.ubiquitousLanguage(Set.of(
"ProductionOrder", "Schedule",
"Resource", "Batch"
))
.build(),
contextMap.getContext("ProductionPlanning")
);
// Quality Assurance Capability
registry.registerCapabilityWithContext(
CapabilityDescriptor.builder()
.name("QualityAssurance")
.implementationClass(QualityAssuranceCapability.class)
.provides(QualityReportContract.class)
.requires(ProductionBatchContract.class)
.requires(QualityDataContract.class)
.ubiquitousLanguage(Set.of(
"QualityCheck", "Defect",
"Specification", "Compliance"
))
.build(),
contextMap.getContext("QualityAssurance")
);
// Material Management Capability
registry.registerCapabilityWithContext(
CapabilityDescriptor.builder()
.name("MaterialManagement")
.implementationClass(MaterialManagementCapability.class)
.provides(MaterialAvailabilityContract.class)
.requires(MaterialRequirementContract.class)
.requires(ExternalERPContract.class)
.ubiquitousLanguage(Set.of(
"Material", "Inventory", "Supplier", "Order"
))
.build(),
contextMap.getContext("MaterialManagement")
);
}
/**
* Establish all contract bindings with pattern validation.
*/
private static void establishContractBindings(
EnhancedCapabilityRegistry registry
) {
// Equipment Health subscribes to Machine Control (Published Language)
registry.bindContractWithPattern(
"EquipmentHealthMonitoring",
"MachineControl",
MachineMonitoringContract.class,
ContextRelationshipPattern.PUBLISHED_LANGUAGE
);
// Production Planning uses Machine Control (Open Host Service)
registry.bindContractWithPattern(
"ProductionPlanning",
"MachineControl",
MachineStatusContract.class,
ContextRelationshipPattern.OPEN_HOST_SERVICE
);
// Production Planning uses Equipment Health (Customer-Supplier)
registry.bindContractWithPattern(
"ProductionPlanning",
"EquipmentHealthMonitoring",
MaintenanceScheduleContract.class,
ContextRelationshipPattern.CUSTOMER_SUPPLIER
);
// Quality Assurance uses Production Planning (Customer-Supplier)
registry.bindContractWithPattern(
"QualityAssurance",
"ProductionPlanning",
ProductionBatchContract.class,
ContextRelationshipPattern.CUSTOMER_SUPPLIER
);
// Partnership: Production Planning <-> Material Management
registry.bindContractWithPattern(
"ProductionPlanning",
"MaterialManagement",
MaterialAvailabilityContract.class,
ContextRelationshipPattern.PARTNERSHIP
);
registry.bindContractWithPattern(
"MaterialManagement",
"ProductionPlanning",
MaterialRequirementContract.class,
ContextRelationshipPattern.PARTNERSHIP
);
}
}
This completes the comprehensive integration of Domain-Driven Design and Capability-Centric Architecture. The tutorial now includes:
- Strategic DDD Patterns: Complete context mapping with all relationship patterns
- Tactical DDD Patterns: All patterns (entities, value objects, aggregates, repositories, services, specifications, factories, domain events) properly placed in the CCA structure
- Complete Capability Implementation: All three layers (Essence, Realization, Adaptation) with proper DDD patterns
- Anticorruption Layers: Full implementation protecting domain models from external systems
- System Assembly: Registry that validates implementation against strategic design
The tutorial demonstrates how DDD and CCA work together seamlessly, with every DDD concept having a clear home in the CCA structure.
No comments:
Post a Comment