Domain Model
Version: 1.0 Date: October 08, 2025 SPDX-License-Identifier: BSD-3-Clause License File: See the LICENSE file in the project root. Copyright: © 2025 Michael Gardner, A Bit of Help, Inc. Authors: Michael Gardner, Claude Code Status: Active
Overview
The domain model is the heart of the pipeline system. It captures the core business concepts, rules, and behaviors using Domain-Driven Design (DDD) principles. This chapter explains how the domain model is structured and why it's designed this way.
Domain-Driven Design Principles
Domain-Driven Design (DDD) is a software development approach that emphasizes:
- Focus on the core domain - The business logic is the most important part
- Model-driven design - The domain model drives the software design
- Ubiquitous language - Shared vocabulary between developers and domain experts
- Bounded contexts - Clear boundaries between different parts of the system
Why DDD?
For a pipeline processing system, DDD provides:
- Clear separation between business logic and infrastructure
- Testable code - Domain logic can be tested without databases or files
- Flexibility - Easy to change infrastructure without touching business rules
- Maintainability - Business rules are explicit and well-organized
Core Domain Concepts
Entities
Entities are objects with a unique identity that persists through time. Two entities are equal if they have the same ID, even if all their other attributes differ.
Pipeline Entity
The central entity representing a file processing workflow.
#![allow(unused)] fn main() { pub struct Pipeline { id: PipelineId, // Unique identity name: String, // Human-readable name stages: Vec<PipelineStage>, // Ordered processing stages configuration: HashMap<String, String>, // Custom settings metrics: ProcessingMetrics, // Performance data archived: bool, // Lifecycle state created_at: DateTime<Utc>, // Creation timestamp updated_at: DateTime<Utc>, // Last modification } }
Key characteristics:
- Has unique
PipelineId
- Can be modified while maintaining identity
- Enforces business rules (e.g., must have at least one stage)
- Automatically adds integrity verification stages
Example:
#![allow(unused)] fn main() { use adaptive_pipeline_domain::Pipeline; // Two pipelines with same ID are equal, even if names differ let pipeline1 = Pipeline::new("Original Name", stages.clone())?; let pipeline2 = pipeline1.clone(); pipeline2.set_name("Different Name"); assert_eq!(pipeline1.id(), pipeline2.id()); // Same identity }
PipelineStage Entity
Represents a single processing operation within a pipeline.
#![allow(unused)] fn main() { pub struct PipelineStage { id: StageId, // Unique identity name: String, // Stage name stage_type: StageType, // Compression, Encryption, etc. configuration: StageConfiguration, // Algorithm and parameters order: usize, // Execution order } }
Stage Types:
Compression
- Data compressionEncryption
- Data encryptionIntegrity
- Checksum verificationCustom
- User-defined operations
ProcessingContext Entity
Manages the runtime execution state of a pipeline.
#![allow(unused)] fn main() { pub struct ProcessingContext { id: ProcessingContextId, // Unique identity pipeline_id: PipelineId, // Associated pipeline input_path: FilePath, // Input file output_path: FilePath, // Output file current_stage: usize, // Current stage index status: ProcessingStatus, // Running, Completed, Failed metrics: ProcessingMetrics, // Runtime metrics } }
SecurityContext Entity
Manages security and permissions for pipeline operations.
#![allow(unused)] fn main() { pub struct SecurityContext { id: SecurityContextId, // Unique identity user_id: UserId, // User performing operation security_level: SecurityLevel, // Required security level permissions: Vec<Permission>, // Granted permissions encryption_key_id: Option<EncryptionKeyId>, // Key for encryption } }
Value Objects
Value Objects are immutable objects defined by their attributes. Two value objects with the same attributes are considered equal.
Algorithm Value Object
Type-safe representation of processing algorithms.
#![allow(unused)] fn main() { pub struct Algorithm(String); impl Algorithm { // Predefined compression algorithms pub fn brotli() -> Self { /* ... */ } pub fn gzip() -> Self { /* ... */ } pub fn zstd() -> Self { /* ... */ } pub fn lz4() -> Self { /* ... */ } // Predefined encryption algorithms pub fn aes_256_gcm() -> Self { /* ... */ } pub fn chacha20_poly1305() -> Self { /* ... */ } // Predefined hashing algorithms pub fn sha256() -> Self { /* ... */ } pub fn blake3() -> Self { /* ... */ } } }
Key characteristics:
- Immutable after creation
- Self-validating (enforces format rules)
- Category detection (is_compression(), is_encryption())
- Type-safe (can't accidentally use wrong algorithm)
ChunkSize Value Object
Represents validated chunk sizes for file processing.
#![allow(unused)] fn main() { pub struct ChunkSize(usize); impl ChunkSize { pub fn new(bytes: usize) -> Result<Self, PipelineError> { // Validates size is within acceptable range if bytes < MIN_CHUNK_SIZE || bytes > MAX_CHUNK_SIZE { return Err(PipelineError::InvalidConfiguration(/* ... */)); } Ok(Self(bytes)) } pub fn from_megabytes(mb: usize) -> Result<Self, PipelineError> { Self::new(mb * 1024 * 1024) } } }
FileChunk Value Object
Immutable representation of a piece of file data.
#![allow(unused)] fn main() { pub struct FileChunk { id: FileChunkId, // Unique chunk identifier sequence: usize, // Position in file data: Vec<u8>, // Chunk data is_final: bool, // Last chunk flag checksum: Option<String>, // Integrity verification } }
FilePath Value Object
Type-safe, validated file paths.
#![allow(unused)] fn main() { pub struct FilePath(PathBuf); impl FilePath { pub fn new(path: impl Into<PathBuf>) -> Result<Self, PipelineError> { let path = path.into(); // Validation: // - Path traversal prevention // - Null byte checks // - Length limits // - Encoding validation Ok(Self(path)) } } }
PipelineId, StageId, UserId (Type-Safe IDs)
All identifiers are wrapped in newtype value objects for type safety:
#![allow(unused)] fn main() { pub struct PipelineId(Ulid); // Can't accidentally use StageId as PipelineId pub struct StageId(Ulid); pub struct UserId(Ulid); pub struct ProcessingContextId(Ulid); pub struct SecurityContextId(Ulid); }
This prevents common bugs like passing the wrong ID to a function.
Domain Services
Domain Services contain business logic that doesn't naturally fit in an entity or value object. They are stateless and operate on domain objects.
Domain services in our system are defined as traits (interfaces) in the domain layer and implemented in the infrastructure layer.
CompressionService
#![allow(unused)] fn main() { #[async_trait] pub trait CompressionService: Send + Sync { async fn compress( &self, data: &[u8], algorithm: &Algorithm, ) -> Result<Vec<u8>, PipelineError>; async fn decompress( &self, data: &[u8], algorithm: &Algorithm, ) -> Result<Vec<u8>, PipelineError>; } }
EncryptionService
#![allow(unused)] fn main() { #[async_trait] pub trait EncryptionService: Send + Sync { async fn encrypt( &self, data: &[u8], algorithm: &Algorithm, key: &EncryptionKey, ) -> Result<Vec<u8>, PipelineError>; async fn decrypt( &self, data: &[u8], algorithm: &Algorithm, key: &EncryptionKey, ) -> Result<Vec<u8>, PipelineError>; } }
ChecksumService
#![allow(unused)] fn main() { pub trait ChecksumService: Send + Sync { fn calculate( &self, data: &[u8], algorithm: &Algorithm, ) -> Result<String, PipelineError>; fn verify( &self, data: &[u8], expected: &str, algorithm: &Algorithm, ) -> Result<bool, PipelineError>; } }
Repositories
Repositories abstract data persistence, allowing the domain to work with collections without knowing about storage details.
#![allow(unused)] fn main() { #[async_trait] pub trait PipelineRepository: Send + Sync { async fn create(&self, pipeline: &Pipeline) -> Result<(), PipelineError>; async fn find_by_id(&self, id: &PipelineId) -> Result<Option<Pipeline>, PipelineError>; async fn find_by_name(&self, name: &str) -> Result<Option<Pipeline>, PipelineError>; async fn update(&self, pipeline: &Pipeline) -> Result<(), PipelineError>; async fn delete(&self, id: &PipelineId) -> Result<(), PipelineError>; async fn list_all(&self) -> Result<Vec<Pipeline>, PipelineError>; } }
The repository interface is defined in the domain layer, but implementations live in the infrastructure layer. This follows the Dependency Inversion Principle.
Domain Events
Domain Events represent significant business occurrences that other parts of the system might care about.
#![allow(unused)] fn main() { pub enum DomainEvent { PipelineCreated { pipeline_id: PipelineId, name: String, created_at: DateTime<Utc>, }, ProcessingStarted { pipeline_id: PipelineId, context_id: ProcessingContextId, input_path: FilePath, }, ProcessingCompleted { pipeline_id: PipelineId, context_id: ProcessingContextId, metrics: ProcessingMetrics, }, ProcessingFailed { pipeline_id: PipelineId, context_id: ProcessingContextId, error: String, }, } }
Events enable:
- Loose coupling - Components don't need direct references
- Audit trails - Track all significant operations
- Integration - External systems can react to events
- Event sourcing - Reconstruct state from event history
Business Rules and Invariants
The domain model enforces critical business rules:
Pipeline Rules
-
Pipelines must have at least one user-defined stage
#![allow(unused)] fn main() { if user_stages.is_empty() { return Err(PipelineError::InvalidConfiguration( "Pipeline must have at least one stage".to_string() )); } }
-
Stage order must be sequential and valid
#![allow(unused)] fn main() { // Stages are automatically reordered: 0, 1, 2, 3... // Input checksum = 0 // User stages = 1, 2, 3... // Output checksum = final }
-
Pipeline names must be unique (enforced by repository)
Chunk Processing Rules
-
Chunks must have non-zero size
#![allow(unused)] fn main() { if size == 0 { return Err(PipelineError::InvalidChunkSize); } }
-
Chunk sequence numbers must be sequential
#![allow(unused)] fn main() { // Chunks are numbered 0, 1, 2, 3... // Missing sequences cause processing to fail }
-
Final chunks must be properly marked
#![allow(unused)] fn main() { if chunk.is_final() { // No more chunks should follow } }
Security Rules
-
Security contexts must be validated
#![allow(unused)] fn main() { security_context.validate()?; }
-
Encryption keys must meet strength requirements
#![allow(unused)] fn main() { if key.len() < MIN_KEY_LENGTH { return Err(PipelineError::WeakEncryptionKey); } }
-
Access permissions must be checked
#![allow(unused)] fn main() { if !security_context.has_permission(Permission::ProcessFile) { return Err(PipelineError::PermissionDenied); } }
Ubiquitous Language
The domain model uses consistent terminology shared between developers and domain experts:
Term | Meaning |
---|---|
Pipeline | An ordered sequence of processing stages |
Stage | A single processing operation (compress, encrypt, etc.) |
Chunk | A piece of a file processed in parallel |
Algorithm | A specific processing method (zstd, aes-256-gcm, etc.) |
Repository | Storage abstraction for domain objects |
Context | Runtime execution state |
Metrics | Performance and operational measurements |
Integrity | Data verification through checksums |
Security Level | Required protection level (Public, Confidential, Secret) |
Testing Domain Logic
Domain objects are designed for easy testing:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn pipeline_enforces_minimum_stages() { // Domain logic can be tested without any infrastructure let result = Pipeline::new("test".to_string(), vec![]); assert!(result.is_err()); } #[test] fn algorithm_validates_format() { // Value objects self-validate let result = Algorithm::new("INVALID-NAME".to_string()); assert!(result.is_err()); let result = Algorithm::new("valid-name".to_string()); assert!(result.is_ok()); } #[test] fn chunk_size_enforces_limits() { // Business rules are explicit and testable let too_small = ChunkSize::new(1); assert!(too_small.is_err()); let valid = ChunkSize::from_megabytes(10); assert!(valid.is_ok()); } } }
Benefits of This Domain Model
- Pure Business Logic - No infrastructure dependencies
- Highly Testable - Can test without databases, files, or networks
- Type Safety - Strong typing prevents many bugs at compile time
- Self-Documenting - Code structure reflects business concepts
- Flexible - Easy to change infrastructure without touching domain
- Maintainable - Business rules are explicit and centralized
Next Steps
Now that you understand the domain model:
- Layered Architecture - How the domain fits into the overall architecture
- Hexagonal Architecture - Ports and adapters pattern
- Repository Pattern - Data persistence abstraction