acc-create-domain-service

Generates DDD Domain Services for PHP 8.5. Creates stateless services for business logic that doesn't belong to entities or value objects. Includes unit tests.

181 stars

Best use case

acc-create-domain-service is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Generates DDD Domain Services for PHP 8.5. Creates stateless services for business logic that doesn't belong to entities or value objects. Includes unit tests.

Teams using acc-create-domain-service should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/acc-create-domain-service/SKILL.md --create-dirs "https://raw.githubusercontent.com/majiayu000/claude-skill-registry/main/skills/data/acc-create-domain-service/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/acc-create-domain-service/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How acc-create-domain-service Compares

Feature / Agentacc-create-domain-serviceStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Generates DDD Domain Services for PHP 8.5. Creates stateless services for business logic that doesn't belong to entities or value objects. Includes unit tests.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Domain Service Generator

Generate DDD-compliant Domain Services for business operations spanning multiple aggregates or requiring external coordination.

## Domain Service Characteristics

- **Stateless**: No internal state, operates on passed arguments
- **Domain Logic**: Contains business rules that don't fit in entities
- **Cross-Aggregate**: Coordinates multiple aggregates
- **Named by Domain Operation**: Verb-based naming (e.g., TransferMoney, CalculateShipping)
- **No Infrastructure**: Pure domain logic, no DB/HTTP calls
- **Immutable Dependencies**: Uses repository interfaces, not implementations

## When to Use Domain Service

| Scenario | Example |
|----------|---------|
| Operation spans multiple aggregates | MoneyTransfer between accounts |
| Complex business calculation | PricingCalculator, TaxCalculator |
| Domain policy enforcement | PasswordPolicy, OrderPolicy |
| Stateless transformation | CurrencyConverter |
| Aggregate coordination | OrderFulfillmentService |

## Template

```php
<?php

declare(strict_types=1);

namespace Domain\{BoundedContext}\Service;

use Domain\{BoundedContext}\Entity\{Entity};
use Domain\{BoundedContext}\ValueObject\{ValueObjects};
use Domain\{BoundedContext}\Repository\{RepositoryInterfaces};
use Domain\{BoundedContext}\Exception\{DomainExceptions};

final readonly class {Name}Service
{
    public function __construct(
        {repositoryDependencies}
    ) {}

    /**
     * @throws {DomainException}
     */
    public function {operation}({parameters}): {ReturnType}
    {
        {domainLogic}
    }

    {privateMethods}
}
```

## Examples

### Money Transfer Service

```php
<?php

declare(strict_types=1);

namespace Domain\Banking\Service;

use Domain\Banking\Entity\Account;
use Domain\Banking\ValueObject\Money;
use Domain\Banking\Repository\AccountRepositoryInterface;
use Domain\Banking\Exception\InsufficientFundsException;
use Domain\Banking\Exception\SameAccountTransferException;

final readonly class MoneyTransferService
{
    public function __construct(
        private AccountRepositoryInterface $accounts
    ) {}

    /**
     * @throws InsufficientFundsException
     * @throws SameAccountTransferException
     */
    public function transfer(
        Account $source,
        Account $destination,
        Money $amount
    ): void {
        if ($source->id()->equals($destination->id())) {
            throw new SameAccountTransferException();
        }

        if (!$source->canWithdraw($amount)) {
            throw new InsufficientFundsException($source->id(), $amount);
        }

        $source->withdraw($amount);
        $destination->deposit($amount);
    }
}
```

### Pricing Calculator Service

```php
<?php

declare(strict_types=1);

namespace Domain\Pricing\Service;

use Domain\Pricing\ValueObject\Money;
use Domain\Pricing\ValueObject\Discount;
use Domain\Pricing\ValueObject\TaxRate;
use Domain\Order\Entity\Order;
use Domain\Customer\Entity\Customer;

final readonly class PricingCalculatorService
{
    public function calculateTotal(
        Order $order,
        Customer $customer,
        ?Discount $discount = null
    ): Money {
        $subtotal = $this->calculateSubtotal($order);
        $discounted = $this->applyDiscount($subtotal, $discount, $customer);
        $taxed = $this->applyTax($discounted, $order->shippingAddress());

        return $taxed;
    }

    private function calculateSubtotal(Order $order): Money
    {
        return $order->items()->reduce(
            fn(Money $total, OrderItem $item) => $total->add(
                $item->price()->multiply($item->quantity())
            ),
            Money::zero($order->currency())
        );
    }

    private function applyDiscount(
        Money $amount,
        ?Discount $discount,
        Customer $customer
    ): Money {
        if ($discount === null) {
            return $amount;
        }

        if (!$discount->isApplicableTo($customer)) {
            return $amount;
        }

        return $discount->apply($amount);
    }

    private function applyTax(Money $amount, Address $address): Money
    {
        $taxRate = TaxRate::forRegion($address->region());
        return $amount->add($amount->multiply($taxRate->value()));
    }
}
```

### Password Policy Service

```php
<?php

declare(strict_types=1);

namespace Domain\User\Service;

use Domain\User\ValueObject\Password;
use Domain\User\ValueObject\PasswordStrength;
use Domain\User\Exception\WeakPasswordException;

final readonly class PasswordPolicyService
{
    private const MIN_LENGTH = 8;
    private const REQUIRED_STRENGTH = PasswordStrength::Strong;

    public function validate(Password $password): void
    {
        $violations = [];

        if ($password->length() < self::MIN_LENGTH) {
            $violations[] = "Password must be at least " . self::MIN_LENGTH . " characters";
        }

        if (!$password->hasUppercase()) {
            $violations[] = "Password must contain uppercase letters";
        }

        if (!$password->hasLowercase()) {
            $violations[] = "Password must contain lowercase letters";
        }

        if (!$password->hasDigit()) {
            $violations[] = "Password must contain digits";
        }

        if (!$password->hasSpecialChar()) {
            $violations[] = "Password must contain special characters";
        }

        if ($password->strength()->isWeakerThan(self::REQUIRED_STRENGTH)) {
            $violations[] = "Password strength must be at least " . self::REQUIRED_STRENGTH->value;
        }

        if ($violations !== []) {
            throw new WeakPasswordException($violations);
        }
    }

    public function calculateStrength(Password $password): PasswordStrength
    {
        $score = 0;

        if ($password->length() >= 12) $score += 2;
        elseif ($password->length() >= 8) $score += 1;

        if ($password->hasUppercase()) $score += 1;
        if ($password->hasLowercase()) $score += 1;
        if ($password->hasDigit()) $score += 1;
        if ($password->hasSpecialChar()) $score += 2;

        return match (true) {
            $score >= 6 => PasswordStrength::Strong,
            $score >= 4 => PasswordStrength::Medium,
            default => PasswordStrength::Weak,
        };
    }
}
```

## Test Template

```php
<?php

declare(strict_types=1);

namespace Tests\Unit\Domain\{BoundedContext}\Service;

use Domain\{BoundedContext}\Service\{Name}Service;
use Domain\{BoundedContext}\Entity\{Entity};
use Domain\{BoundedContext}\ValueObject\{ValueObject};
use Domain\{BoundedContext}\Exception\{DomainException};
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

#[Group('unit')]
#[CoversClass({Name}Service::class)]
final class {Name}ServiceTest extends TestCase
{
    private {Name}Service $service;

    protected function setUp(): void
    {
        $this->service = new {Name}Service(
            {mockDependencies}
        );
    }

    public function test{Operation}Successfully(): void
    {
        {arrange}

        $result = $this->service->{operation}({parameters});

        {assert}
    }

    public function test{Operation}ThrowsOn{Condition}(): void
    {
        {arrange}

        $this->expectException({DomainException}::class);

        $this->service->{operation}({invalidParameters});
    }

    {additionalTests}
}
```

### Example Test

```php
<?php

declare(strict_types=1);

namespace Tests\Unit\Domain\Banking\Service;

use Domain\Banking\Service\MoneyTransferService;
use Domain\Banking\Entity\Account;
use Domain\Banking\ValueObject\AccountId;
use Domain\Banking\ValueObject\Money;
use Domain\Banking\Exception\InsufficientFundsException;
use Domain\Banking\Exception\SameAccountTransferException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

#[Group('unit')]
#[CoversClass(MoneyTransferService::class)]
final class MoneyTransferServiceTest extends TestCase
{
    private MoneyTransferService $service;

    protected function setUp(): void
    {
        $this->service = new MoneyTransferService(
            $this->createMock(AccountRepositoryInterface::class)
        );
    }

    public function testTransfersMoneyBetweenAccounts(): void
    {
        $source = $this->createAccountWithBalance(Money::USD(1000));
        $destination = $this->createAccountWithBalance(Money::USD(500));
        $amount = Money::USD(300);

        $this->service->transfer($source, $destination, $amount);

        self::assertTrue($source->balance()->equals(Money::USD(700)));
        self::assertTrue($destination->balance()->equals(Money::USD(800)));
    }

    public function testThrowsOnInsufficientFunds(): void
    {
        $source = $this->createAccountWithBalance(Money::USD(100));
        $destination = $this->createAccountWithBalance(Money::USD(500));
        $amount = Money::USD(300);

        $this->expectException(InsufficientFundsException::class);

        $this->service->transfer($source, $destination, $amount);
    }

    public function testThrowsOnSameAccountTransfer(): void
    {
        $account = $this->createAccountWithBalance(Money::USD(1000));

        $this->expectException(SameAccountTransferException::class);

        $this->service->transfer($account, $account, Money::USD(100));
    }

    private function createAccountWithBalance(Money $balance): Account
    {
        $account = new Account(AccountId::generate());
        $account->deposit($balance);
        return $account;
    }
}
```

## Naming Conventions

| Pattern | Example |
|---------|---------|
| Service | `{Operation}Service` | `MoneyTransferService`, `PricingCalculatorService` |
| Method | `{verb}{noun}` | `transfer()`, `calculate()`, `validate()` |
| Exception | `{Condition}Exception` | `InsufficientFundsException` |
| Test | `{ServiceName}Test` | `MoneyTransferServiceTest` |

## File Placement

| Component | Path |
|-----------|------|
| Domain Service | `src/Domain/{BoundedContext}/Service/` |
| Exceptions | `src/Domain/{BoundedContext}/Exception/` |
| Unit Tests | `tests/Unit/Domain/{BoundedContext}/Service/` |

## Anti-patterns to Avoid

| Anti-pattern | Problem | Solution |
|--------------|---------|----------|
| Anemic Service | Just delegates to entities | Move logic to entities |
| Infrastructure in Service | DB/HTTP calls | Use repository interfaces |
| Stateful Service | Maintains internal state | Make stateless |
| God Service | Too many responsibilities | Split into focused services |
| Business Logic in Constructors | Complex setup | Keep constructors simple |

Related Skills

add-service

181
from majiayu000/claude-skill-registry

새 API 서비스를 생성합니다. service + hook + type 세트를 함께 생성합니다. 사용법: /add-service EntityName

add-k8s-service

181
from majiayu000/claude-skill-registry

往 Kubernetes 集群添加普通服务。当用户请求部署新应用、添加新服务到集群时使用此技能。

add-domain-entity

181
from majiayu000/claude-skill-registry

Create domain layer components: models, repository interfaces, marshallers, and implementations. Use when: (1) adding domain model in internal/domain/model/, (2) creating repository interface in internal/domain/repository/, (3) implementing repository with marshaller in internal/infrastructure/{db}/. This is Step 2 of CRUD workflow (after add-database-table, before add-api-endpoint).

add-background-service

181
from majiayu000/claude-skill-registry

Create BackgroundService implementations for scheduled or polling tasks (project)

acc-create-value-object

181
from majiayu000/claude-skill-registry

Generates DDD Value Objects for PHP 8.5. Creates immutable, self-validating objects with equality comparison. Includes unit tests.

acc-create-use-case

181
from majiayu000/claude-skill-registry

Generates Application Use Cases for PHP 8.5. Creates orchestration services that coordinate domain objects, handle transactions, and dispatch events. Includes unit tests.

acc-create-unit-test

181
from majiayu000/claude-skill-registry

Generates PHPUnit unit tests for PHP 8.5. Creates isolated tests with AAA pattern, proper naming, attributes, and one behavior per test. Supports Value Objects, Entities, Services.

acc-create-test-double

181
from majiayu000/claude-skill-registry

Generates test doubles (Mocks, Stubs, Fakes, Spies) for PHP 8.5. Creates appropriate double type based on testing needs with PHPUnit MockBuilder patterns.

acc-create-test-builder

181
from majiayu000/claude-skill-registry

Generates Test Data Builder and Object Mother patterns for PHP 8.5. Creates fluent builders with sensible defaults and factory methods for test data creation.

acc-create-strategy

181
from majiayu000/claude-skill-registry

Generates Strategy pattern for PHP 8.5. Creates interchangeable algorithm families with context class, strategy interface, and concrete implementations. Includes unit tests.

acc-create-state

181
from majiayu000/claude-skill-registry

Generates State pattern for PHP 8.5. Creates state machines with context, state interface, and concrete states for behavior changes. Includes unit tests.

acc-create-specification

181
from majiayu000/claude-skill-registry

Generates DDD Specification for PHP 8.5. Creates reusable business rule objects for validation, filtering, and querying with composite pattern support. Includes unit tests.