A pattern without a name
Over the past few years, working across different teams and codebases, I kept noticing the same thing: engineers who had tried Clean DDD and retreated, and engineers who had outgrown Active Record, both ended up writing code that looked roughly the same. A domain model that held behaviour. Infrastructure dependencies pointing inward. No full ports-and-adapters ceremony, but not an ORM model with business logic sprinkled into controllers either.
Nobody called it anything. It was just "how we write code here."
I started calling it Domain-Composed Models (DCM), not because it needed branding, but because naming a pattern makes it easier to teach, debate, and deliberately choose.
The problem with the extremes
Active Record is fast to start. The model knows how to persist itself, query itself, and often validate itself. For simple CRUD-heavy applications it is genuinely appropriate. The problem arrives when the domain gets complex: business rules start accumulating in service classes, the model becomes anemic, and you end up with UserManager, OrderProcessor, and PaymentService each holding fragments of logic that belong together.
Clean DDD solves this. Domain logic lives in rich entities and aggregates, completely isolated from infrastructure concerns behind repository interfaces and application service layers. It is correct. It is also expensive — expensive to learn, expensive to set up, expensive to maintain when the problem does not justify the ceremony.
The friction point is real: most teams are not building a financial trading system or a complex insurance domain. They are building a SaaS product with meaningful business rules that deserve better than anemic models, but not a full hexagonal architecture.
What DCM looks like
The pattern has three properties:
Domain behaviour lives on the model. Not in service classes. A Subscription knows how to renew, cancel, and check its own eligibility. This eliminates anemia without requiring an aggregate root hierarchy.
Infrastructure goes one way. The domain model does not import from the ORM, the messaging layer, or the database client. Persistence adapters depend on domain objects, not the reverse. This is lighter than full Clean DDD — there are no repository interfaces — but it preserves the directional constraint that makes domain logic independently testable.
Composition over inheritance for cross-cutting concerns. Instead of a base ActiveRecord class that drags infrastructure into every model, concerns like audit trails, soft deletion, or event emission are mixed in explicitly where they are needed.
# A DCM-style subscription model (Python/SQLAlchemy flavour)
class Subscription:
def __init__(self, plan: Plan, started_at: datetime):
self.plan = plan
self.started_at = started_at
self.status = SubscriptionStatus.ACTIVE
def renew(self, as_of: datetime) -> "Subscription":
if not self.plan.allows_renewal(as_of):
raise RenewalNotAllowed(self.plan, as_of)
return Subscription(plan=self.plan, started_at=as_of)
def cancel(self, reason: CancellationReason) -> None:
self.status = SubscriptionStatus.CANCELLED
self._record(SubscriptionCancelled(reason=reason))
# Persistence is a separate concern that depends on Subscription,
# not the other way around
class SubscriptionRepository:
def save(self, subscription: Subscription) -> None: ...
def find_by_id(self, id: UUID) -> Subscription | None: ...
When to use it
DCM earns its place when:
- The domain has real rules that get violated when logic scatters into service classes
- The team is comfortable with OOP but not prepared to invest in a full hexagonal architecture
- Testing matters — domain behaviour should be testable without database fixtures
It is not appropriate for simple CRUD operations where the "domain" is really just data shapes. Active Record is fine there. The right amount of structure is always determined by the actual complexity of the problem, not by cargo-culting patterns from domains with different constraints.
The full writeup with a comparison table across Active Record, DCM, and Clean DDD is on Medium.