AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MPL-2.0 Self-run

Domain Modeling

skill-garrettw-php-arch-skills-domain-modeling · by garrettw

Use this skill when deciding whether to use rich domain objects vs lean ORM entities, choosing Transaction Script vs Table Module vs a Domain Model, identifying real business objects, or evaluating the cost-benefit of full DDD vs lean structure in a PHP application.

No reviews yet
0 installs
11 views
0.0% view→install

Install

$ agentstack add skill-garrettw-php-arch-skills-domain-modeling

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-garrettw-php-arch-skills-domain-modeling)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
23d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Domain Modeling? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Domain Modeling for PHP Apps

When to Use (and When NOT to)

DDD adds ceremony that must justify its cost. Use DDD when the project has a complex business domain with many rules and will be maintained for years by a team. Skip DDD for prototyping, MVPs, solo-developer projects, or simple CRUD with few business rules.

| Use DDD For | Use Simpler Patterns For | |----------------------------------------------|----------------------------------------------| | Complex business domain with many invariants | Simple CRUD, few business rules | | Long-lived system (years of maintenance) | Prototype, MVP, throwaway code | | Team of multiple developers | Solo developer or tiny team (1-2) | | Multiple entry points (API, CLI, events) | Single entry point, simple API | | Need to swap infrastructure (DB, broker) | Fixed infrastructure, unlikely to change | | High test coverage required | Quick scripts, internal tools |

Start simple. Evolve complexity only when needed. Most systems do not need full DDD. For libraries, packages, or SDKs that have no user-facing behavior, plain PSR-4 classes with clear names are usually sufficient. For simple CRUD, lean ORM entities are preferable to rich domain objects.

Note: the choices below (Transaction Script, Table Module, Domain Model) are about how business rules are organized. The Application (or Service) Layer sits above this spectrum — it defines the application's boundary and coordinates whichever domain-logic style you pick. See the application-layer skill for Service Layer definition and the overuse anti-pattern (Controller → Service → Repository → Entity with an anemic domain).

You do not pick one style for the whole codebase

An application does not have to commit to a single domain-modeling style everywhere. Different areas of a system have different complexity, and the right pattern varies by area:

  • A reporting/reporting-adjacent area may be best as a Table Module (set-oriented, data-centric).
  • A simple admin or CRUD feature may be best as a Transaction Script.
  • The core, rule-heavy part of the business (pricing, billing, entitlements) may warrant a rich Domain Model.

Choose the lightest pattern that fits that area's rules, and let each area evolve independently toward a heavier pattern as its rules demand. This is exactly the job of bounded contexts (see the bounded-contexts skill): a context is a natural boundary inside which one domain-logic style can be the default without forcing it on the rest of the system. Do not let a heavy pattern leak from a complex context into a simple one, and do not force a simple pattern onto a context that clearly needs rich behavior.

Transaction Script: the simpler default

Before reaching for a rich Domain Model, consider a [Transaction Script](references/transaction-script.md). A Transaction Script is a single procedure per request that orchestrates the steps (validate → fetch → calculate → save) and talks to the database through a thin gateway. It is the lightest way to organize business logic and the right default for simple CRUD, linear request/response flows, and prototypes. Upgrade to a Domain Model only when the same rules start repeating across scripts and drifting out of sync.

Table Module: the middle ground

When logic is shaped around sets of rows in a table rather than individual objects, a [Table Module](references/table-module.md) is the compromise between Transaction Script and a Domain Model. It is one class per table whose single instance operates over a recordset of all rows, bundling behavior with the data without paying the O/R-mapping cost of a Domain Model. Use it for table-centric logic (totals, summaries, batch updates, data-grid/report UIs). Move to a Domain Model only when rows need their own identity, relationships, or polymorphic behavior.

System Overview

Use Domain-Driven Design (DDD) to make business rules explicit, not to add ceremony. Every backend abstraction should justify itself by reducing complexity, protecting an invariant, improving locality, or making tests clearer. This skill guides the decision process for introducing rich domain objects and structuring business operations.

Numbered Workflows

1. Identifying the Real Business Object

If the user asks to model a new noun or entity:

  1. Challenge the initial noun. Ask if the core concept is actually an operation, lifecycle, policy, or transaction that keeps several facts consistent.
  2. Reframe around events. Instead of asking "Can this user do X?", ask "Can this operation happen?"
  3. If the object is just a data shell, do not give it domain rules. See step 2.

2. Choosing Transaction Script vs a Rich Domain Model

If you are deciding how to organize a feature's business logic:

  1. Prefer Transaction Script by default. If the feature is a linear, single-path procedure (read some data, validate, write a result) with few or no business rules, implement it as a Transaction Script (one procedure per request, thin DB gateway). Do not introduce a rich domain object.
  2. Watch for duplication. If the same validation, calculation, or policy already appears in more than one script (or is about to), that behavior belongs on a shared domain object, not copied across scripts. Upgrade then.
  3. Consider a Table Module for row-set logic. If the feature computes over many rows of one table (totals, summaries, batch updates) or backs a data-grid/report UI, use a Table Module: one class per table operating on a recordset, paired with a thin gateway. Move to a Domain Model once the rules become individualized per row (per-customer pricing, tax exemptions, regional rules, contract terms, subscriptions).
  4. Otherwise evaluate Domain Behavior. Does the feature have meaningful invariants, business rules, state transitions, or cross-context policy that need object identity and relationships?
  5. Evaluate Testability. Would the core rule be easier to test in-memory without booting the framework or database?
  6. If Yes to either: Propose building an active domain object. Note that this will require separating persistence into mappers and repositories (see the persistence-patterns skill).
  7. If No to both: Propose using lean ORM entities directly. Do not build a separate pure PHP domain object.

3. Structuring Services and Handlers

If you are writing an application service or handler:

  1. Load data: Retrieve required state from repositories or adapters.
  2. Decide: Pass the data to an active domain object to make the business decision.
  3. Persist: Save the result via the repository.

4. Defining Aggregate Boundaries

If deciding whether related entities belong in the same aggregate:

  1. Transaction Consistency. Must they be consistent together in a single transaction? If yes, place them in the same aggregate.
  2. Reference by ID Only. If they would be referenced directly (not by ID), they belong in the same aggregate.
  3. Split Large Aggregates. If an aggregate contains more than 10 entities, split it into smaller aggregates and use domain events for eventual consistency.
  4. Rule: One aggregate per transaction. Cross-aggregate consistency is handled via domain events, not distributed transactions.

5. Modeling Exceptional-but-Valid States

If a query or method might return "nothing" or a degenerate state (null customer, empty cart, no discount):

  1. Prefer a Special Case over null. Return an object implementing the same interface as the real one — GuestUser, AnonymousCustomer, EmptyCart, NoDiscount — so callers skip the if ($x !== null) ceremony (see [special-case.md](references/special-case.md)).
  2. Name the state, don't hide absence. NoDiscount is a real, named business state; model it as a first-class object, not a missing value.
  3. Don't hide errors. Only use a Special Case for a valid domain outcome. If the situation is a failure (lookup failed, invariant broken, timeout), surface it as an exception or error result — never mask it behind a Special Case.
  • Bigger picture: Special Case protects the behavioral boundary — one of a family of boundary-protection patterns (with Gateway, Mapper, Remote Facade, DTO, Plugin) that underlie Hexagonal architecture. See [boundary-protection-patterns.md](../distribution-patterns/references/boundary-protection-patterns.md).

Recognizing Problems in an Existing Model

When analyzing an established codebase, these are the domain-modeling smells to look for. Each is a signal, not an automatic defect — judge it against the area's real complexity before recommending change:

  • Anemic domain with logic in services. Entities are data shells; all rules live in Service → Repository → Entity flows. Migrate the rules down only when you next touch that area (see the architecture-migration skill's trigger rule — never refactor untouched code for tidiness).
  • Business logic in ORM callbacks / model events. Rules fire inside saving, created, or entity lifecycle hooks, where they can't be tested without the framework and bypass the application layer's transaction/side-effect control.
  • null returned for a valid absence. A missing customer, empty cart, or no discount is modeled as null, forcing if ($x !== null) at every call site. Replace with a Special Case object.
  • Wrong pattern for the area's complexity. A rich Domain Model forced onto simple CRUD (ceremony without payoff), or a Transaction Script stretched far past its useful life (the same validation/rule duplicated across many scripts, drifting out of sync).
  • Repository per entity. One repo per entity instead of per aggregate root, which lets callers bypass aggregate consistency boundaries.

Boundaries

Always Do

  • Always evaluate whether the abstraction justifies its cost before introducing a mapper or domain interface.
  • Always ensure active domain objects can be tested with plain PHP, without booting the framework.
  • Always read the [Decision Flowchart](references/decision-flowchart.md) before deciding on a structure.
  • Always consider a [Transaction Script](references/transaction-script.md) before reaching for a rich Domain Model; it is the lighter default for simple, linear procedures.
  • Always consider a [Table Module](references/table-module.md) when logic operates over sets of rows in a single table, before paying for a full Domain Model's O/R mapping.
  • Always use "unique identity that persists" → Entity, "defined only by attributes" → Value Object when deciding between the two.
  • Always model an exceptional-but-valid state as a Special Case object ([special-case.md](references/special-case.md)) instead of returning null, but never use a Special Case to mask a genuine error.

Ask First

  • Ask before extracting intermediate Command classes, Request DTOs, or "domain services" unless they carry meaningful domain data.
  • Ask before splitting a lean CRUD entity into a full DDD aggregate.

Never Do

  • Never chain services together (e.g., Service A calls Service B calls Service C). Each handler should own one complete business result.
  • Never place core business rules in framework controllers, ORM callbacks, or provider adapters.
  • Never build a repository per entity. Use one repository per aggregate root to preserve consistency boundaries.

Related Patterns

  • Behavioral/structural patterns used in the domain — Strategy, Builder, Composite, State, Visitor, Observer — are catalogued in [behavioral-structural-patterns.md](references/behavioral-structural-patterns.md).

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.