Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Domain Model pattern puts business data and the rules that govern it together in a model of the problem your software solves. In PHP, it is most useful when business rules are complex, change often, or are easy to violate—not as a required layer for every application.

What is the Domain Model pattern?

Martin Fowler defines a Domain Model as “an object model of the domain that incorporates both behavior and data.” The model represents meaningful business concepts and gives those concepts responsibility for rules tied to their state. Instead of scattering a rule across controllers, database callbacks, and utility functions, the model gives it a clear home. Fowler’s pattern description dates to 5 March 2003: Domain Model.

For example, an order might know whether it can accept another line, while a subscription might decide whether cancellation is allowed in its current state. These objects do more than carry fields: their methods express valid business actions. Domain-Driven Design (DDD) builds on this approach by emphasizing a model and vocabulary that reflect the business, particularly where processes and rules are complex. See Fowler’s Domain-Driven Design overview, dated 22 April 2020.

When should you use a rich domain model?

Choose a rich model when important rules are numerous, likely to change, span related objects, or would be costly to enforce consistently if they were scattered. A method such as $subscription->cancel($reason) can check the subscription’s state and apply a valid transition in one place. A rich model makes the rule easier to find and test; it does not mean every property needs elaborate behavior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A simple CRUD application may be better served by a Transaction Script: a focused procedure handles a request and performs the required work. Fowler lists Transaction Script alongside Domain Model as a domain-logic pattern. Other options have different trade-offs:

Pattern How it organizes behavior When it may fit
Transaction Script A procedure handles a use case. Straightforward workflows or CRUD with few rules.
Active Record An object represents a database row and includes persistence behavior. Applications where the object-to-table mapping is close and storage coupling is acceptable.
Table Module One object holds business logic for rows in a table or view. Data-centric applications with limited need for object identity.
Domain Model with Repository or Data Mapper Objects express business rules; persistence and mapping are kept behind boundaries. Rules are numerous, change frequently, or do not fit neatly into table operations.

These patterns are alternatives, not a maturity ladder. Fowler’s overview of enterprise application patterns describes them as distinct approaches to domain logic. Start with rules that are expensive to change or easy to violate. Add model boundaries where they protect those rules; avoid adopting DDD terminology simply to label ordinary code.

How the main domain-model concepts fit together

Entities

An entity has an identity that remains meaningful while its state changes. An Order can be edited or shipped and still be the same order. Give it operations that protect its lifecycle rather than exposing unrestricted setters.

Value objects

A value object is defined by its value rather than by a continuing identity. Examples include Money, EmailAddress, and DateRange. A well-designed value object validates its inputs so invalid values are difficult to create and carries the rules relevant to that concept.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Aggregate roots

An aggregate is a group of related objects governed by consistency rules; its root is the entry point for changes to that group. If an order’s lines and total must remain consistent, callers should change them through the root rather than editing internal objects independently. Define the aggregate boundary around invariants that must hold together, and make the corresponding transaction boundary explicit.

Domain services

A domain service expresses stateless business logic that involves multiple objects when no single entity naturally owns the operation. It belongs to the domain because the operation is a business rule, not because it calls a database or an external API.

Domain events

A domain event records a business fact that has already happened, such as an order being placed. Events can help decouple reactions when the domain needs that capability, but they add concepts and coordination. Use them for a real integration or modeling need rather than as a default for every state change.

Repositories

A repository offers a collection-like interface for loading and saving aggregates. Its interface can sit at the domain or application boundary, while a concrete implementation belongs in infrastructure, where database and ORM details live. Fowler describes Repository as mediation between the domain and data-mapping layers: Repository.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How to keep business logic out of PHP framework controllers

Use controllers for HTTP concerns: receive a request, translate its input, invoke an application use case, and return a response. Application services coordinate the work by loading the relevant aggregate, calling its domain behavior, and saving the result. The domain should not depend on a Laravel or Symfony request object, controller, ORM base class, or database schema.

A useful dependency direction is presentation to application to domain, with infrastructure adapters providing persistence and other technical details from the outside. Fowler’s discussion of separating presentation from domain and data sources explains why domain logic should be considered independently of its UI and persistence. Microsoft’s archived Domain Model guidance likewise emphasizes minimizing coupling so business behavior can be changed, built, and tested more easily.

The result is not a rule that every application must have four elaborate directories. It is a boundary: framework and transport details should not dictate what a business operation means. Keep framework-specific mapping at the edge where practical, using adapters or mappers when needed.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A practical workflow for building the model

  1. Describe the use case in business language. Identify the concepts, actions, rules, and lifecycle states involved. For example, distinguish “cancel an eligible subscription” from the HTTP endpoint that receives a cancellation request.
  2. Separate identity from value. Decide which concepts persist as identifiable entities and which are interchangeable values such as an email address or money amount.
  3. Set aggregate boundaries around invariants. Keep together the state that must change consistently, and make clear which root controls changes.
  4. Express state changes as intention-revealing methods. Prefer methods such as $order->addLine($line) to public setters that let callers create invalid combinations. Validate preconditions where the transition occurs.
  5. Define persistence interfaces at a boundary. Put a repository interface in the domain or application boundary when it helps keep persistence concerns out of business logic; implement it in infrastructure.
  6. Keep framework and ORM concerns at the edge. Use an adapter or mapper where practical so domain objects do not need to inherit from framework classes or understand database schema details.
  7. Test domain rules independently. Unit-test the model without a database or HTTP server, then test persistence adapters and framework integration separately.
  8. Revisit the design as rules change. Add a repository, service, event, or aggregate boundary when it solves a real problem, not to complete a pattern checklist.

How to tell whether the model is earning its complexity

A rich domain model is useful when it makes rules clearer, more consistent, and easier to change than the alternatives. Evaluate a design against the nature of the rules, coupling to persistence, testability, transaction boundaries, and the team’s familiarity with the patterns. A small application can benefit from one or two focused value objects or entities without adopting every DDD tactical pattern.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • If business rules are simple and track closely to a request, a Transaction Script may be sufficient.
  • If persistence conventions dominate the objects, consider whether Active Record’s convenience is worth its storage coupling.
  • If rules must hold across related objects, define an aggregate root and explicit consistency boundary.
  • If logic genuinely spans multiple entities, consider a domain service rather than forcing it into an unnatural owner.
  • If repository interfaces only add indirection without isolating a meaningful persistence boundary, do not add them by default.

For further PHP-focused reading, O’Reilly hosts Domain-Driven Design in PHP, which covers PHP architecture and concepts including entities, events, repositories, and ubiquitous language.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.