| name | domain-modeling |
| description | Design domain models following parse-once principle with type-safe APIs, factory methods, and Repository pattern. Use when creating domain models, parsing data structures, or organizing business logic in models. |
| user-invocable | true |
| argument-hint | [model-name] |
Domain Modeling
When to Use This Skill
Activate this skill when:
- Creating domain models that encapsulate parsing logic
- Working with structured data (YAML, JSON, XML, markdown)
- Converting raw strings/dictionaries into type-safe objects
- Implementing the Repository pattern
- Centralizing data validation and parsing
Quick Reference
Parse-Once Pattern
@dataclass
class ProjectConfiguration:
"""Parsed configuration with validation"""
project: Project
reviewers: List[Reviewer]
@classmethod
def from_yaml_string(cls, project: Project, content: str) -> 'ProjectConfiguration':
"""Parse once, validate, return type-safe model"""
config = yaml.safe_load(content)
if "reviewers" not in config:
raise ConfigurationError("Missing reviewers field")
reviewers = [
Reviewer(username=r["username"], max_open_prs=r.get("maxOpenPRs", 2))
for r in config["reviewers"]
if "username" in r
]
return cls(project=project, reviewers=reviewers)
class ProjectRepository:
def load_configuration(self, project: Project) -> Optional[ProjectConfiguration]:
content = get_file_from_branch(self.repo, project.config_path)
return ProjectConfiguration.from_yaml_string(project, content)
config = repository.load_configuration(project)
usernames = [r.username for r in config.reviewers]
Principle: Parse Once Into Well-Formed Models
When working with structured data (YAML files, JSON responses, markdown files, etc.), parse the data once into a well-formed domain model rather than passing raw strings or dictionaries around and parsing them repeatedly.
Anti-Pattern (❌ Avoid)
class StatisticsService:
def collect_project_stats(self, project_name: str):
config_path = f"claude-chain/{project_name}/configuration.yml"
spec_path = f"claude-chain/{project_name}/spec.md"
config_content = get_file_from_branch(repo, branch, config_path)
spec_content = get_file_from_branch(repo, branch, spec_path)
config = yaml.safe_load(config_content)
reviewers_config = config.get("reviewers", [])
reviewers = [r.get("username") for r in reviewers_config if "username" in r]
total = len(re.findall(r"^\s*- \[[xX \]]", spec_content, re.MULTILINE))
completed = len(re.findall(r"^\s*- \[[xX]\]", spec_content, re.MULTILINE))
return (total, completed, reviewers)
Problems with this approach:
- Parsing logic scattered: Different services duplicate regex patterns and YAML parsing
- No type safety: Dictionary access with string keys can fail silently
- Brittle: Changes to file format require updates in multiple places
- Wrong layer: Business logic (parsing) mixed with orchestration (service layer)
- Hard to test: Must test parsing logic alongside business logic
- Poor reusability: Can't reuse parsing logic across services
Recommended Pattern (✅ Use This)
@dataclass
class Project:
"""Domain model representing a ClaudeChain project"""
name: str
@property
def config_path(self) -> str:
"""Centralized path construction"""
return f"claude-chain/{self.name}/configuration.yml"
@property
def spec_path(self) -> str:
return f"claude-chain/{self.name}/spec.md"
@dataclass
class Reviewer:
"""Type-safe reviewer model"""
username: str
max_open_prs: int = 2
@dataclass
class ProjectConfiguration:
"""Parsed configuration with validation"""
project: Project
reviewers: List[Reviewer]
@classmethod
def from_yaml_string(cls, project: Project, content: str) -> 'ProjectConfiguration':
"""Parse once, validate, return type-safe model"""
config = yaml.safe_load(content)
config:
ConfigurationError()
reviewers = [
Reviewer(
username=r[],
max_open_prs=r.get(, )
)
r config[]
r
]
cls(project=project, reviewers=reviewers)
() -> []:
[r.username r .reviewers]
:
index:
description:
is_completed:
:
():
.project = project
.content = content
._tasks: [[SpecTask]] =
() -> [SpecTask]:
._tasks :
._tasks = ._parse_tasks()
._tasks
() -> [SpecTask]:
tasks = []
task_index =
line .content.split():
= re.(, line)
:
tasks.append(SpecTask(
index=task_index,
description=.group().strip(),
is_completed=.group().lower() ==
))
task_index +=
tasks
() -> :
(.tasks)
() -> :
( task .tasks task.is_completed)
:
():
.repo = repo
() -> [ProjectConfiguration]:
content = get_file_from_branch(.repo, branch, project.config_path)
content:
ProjectConfiguration.from_yaml_string(project, content)
() -> [SpecContent]:
content = get_file_from_branch(.repo, branch, project.spec_path)
content:
SpecContent(project, content)
:
():
.repo = repo
.metadata_service = metadata_service
.project_repository = project_repository
() -> ProjectStats:
project = Project(project_name)
config = .project_repository.load_configuration(project)
spec = .project_repository.load_spec(project)
config spec:
stats = ProjectStats(project_name)
stats.total_tasks = spec.total_tasks
stats.completed_tasks = spec.completed_tasks
stats.reviewers = config.get_reviewer_usernames()
stats
Benefits
✅ Parse once: Data is parsed into models at the boundary (repository/infrastructure layer)
✅ Type safety: Domain models provide strongly-typed properties and methods
reviewers = config.reviewers
username = reviewers[0].username
✅ Centralized parsing: Regex patterns and parsing logic in one place
total = spec.total_tasks
pending = spec.pending_tasks
✅ Clear layering:
- Infrastructure: Fetches raw data from external systems
- Domain: Parses into validated, type-safe models
- Service: Orchestrates domain models (no parsing!)
✅ Testability: Test parsing separately from business logic
def test_spec_content_parsing():
spec = SpecContent(project, "- [ ] Task 1\n- [x] Task 2")
assert spec.total_tasks == 2
assert spec.completed_tasks == 1
def test_statistics_service():
mock_spec = Mock(total_tasks=10, completed_tasks=5)
mock_repo.load_spec.return_value = mock_spec
✅ Validation at boundaries: Models validate on construction
@classmethod
def from_yaml_string(cls, project: Project, content: str):
config = yaml.safe_load(content)
if "reviewers" not in config:
raise ConfigurationError("Missing reviewers")
return cls(...)
✅ Reusability: Models can be used across different services
class StatisticsService:
def collect_stats(self, project: Project):
config = self.repo.load_configuration(project)
class ReviewerService:
def assign_reviewer(self, project: Project):
config = self.repo.load_configuration(project)
Architecture Pattern
┌─────────────────────────────────────────────────────────────┐
│ External System (GitHub API, File System) │
│ - Returns: Raw strings (YAML, JSON, Markdown) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer (Repository Pattern) │
│ - Fetches raw data from external systems │
│ - Delegates parsing to domain model factories │
│ - Returns: Fully-parsed domain models │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer (Models with Factory Methods) │
│ - Project, ProjectConfiguration, SpecContent │
│ - Encapsulates parsing logic (regex, YAML, validation) │
│ - Provides type-safe APIs │
│ - Single source of truth for data structure │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer (Business Logic) │
│ - Receives parsed domain models │
│ - Uses type-safe model APIs │
│ - No string parsing or dictionary access │
│ - Focuses on orchestration and business rules │
└─────────────────────────────────────────────────────────────┘
Key Principles
- Parse at boundaries: Convert raw data to domain models as soon as it enters your system
- Parse once: Never re-parse the same data in multiple places
- Domain owns parsing: Parsing logic belongs in domain models, not services
- Type-safe APIs: Models expose strongly-typed properties and methods
- Validate early: Domain model constructors/factories validate structure
- No leaky abstractions: Services don't know about YAML, regex, or file formats
Related Patterns
This approach aligns with:
- Repository Pattern: Infrastructure fetches, domain parses
- Factory Pattern:
from_yaml_string(), from_branch_name() constructors
- Domain-Driven Design: Rich domain models with behavior
- Dependency Inversion: Services depend on domain abstractions, not infrastructure
Related Skills
- creating-services: Learn how to use domain models in service classes
- testing-services: Understand how to test domain models and services that use them
- identifying-layer-placement: Learn where domain models fit in the architecture
Further Reading