- DDD
- Claude
- AI
DDD with Claude Code: Teaching AI to Think in Bounded Contexts
How to use Claude Code with AI Craftsman Superpowers to generate domain models that respect aggregate boundaries and ubiquitous language.
Alexandre Mallet2 min read
Most AI coding assistants treat your codebase as a flat list of files. Ask for an entity and you'll get a class with public properties, getters, setters, and zero domain logic. It "works": the same way a cardboard sword works. It has the shape, but none of the strength.
DDD tactical patterns exist because decades of real-world projects proved that anemic models collapse under complexity. AI Craftsman Superpowers teaches Claude to think in bounded contexts, not just files.
The Problem with Naive Entity Generation
Ask a vanilla AI to create a Subscription entity and you'll typically get:
class Subscription
{
private string $id;
private string $userId;
private string $plan;
private string $status;
private \DateTimeImmutable $startDate;
private ?\DateTimeImmutable $endDate;
public function getStatus(): string { return $this->status; }
public function setStatus(string $status): void { $this->status = $status; }
public function getPlan(): string { return $this->plan; }
public function setPlan(string $plan): void { $this->plan = $plan; }
}This is a data bag. There's no invariant protection, no domain events, no ubiquitous language. Anyone can call setStatus('cancelled') without checking if the subscription is already expired. The domain rules live in a service somewhere, or worse, in a controller.
The Craftsman Approach
With AI Craftsman Superpowers, the /craftsman:entity command scaffolds entities that enforce DDD tactical patterns. The same Subscription becomes:
declare(strict_types=1);
final class Subscription
{
private function __construct(
private readonly SubscriptionId $id,
private readonly UserId $userId,
private readonly Plan $plan,
private SubscriptionStatus $status,
private readonly \DateTimeImmutable $startedAt,
private ?\DateTimeImmutable $cancelledAt,
) {}
public static function start(
SubscriptionId $id,
UserId $userId,
Plan $plan,
\DateTimeImmutable $now,
): self {
$subscription = new self(
$id,
$userId,
$plan,
SubscriptionStatus::Active,
$now,
null,
);
$subscription->raise(new SubscriptionStarted($id, $userId, $plan));
return $subscription;
}
public function cancel(\DateTimeImmutable $now): void
{
if ($this->status === SubscriptionStatus::Cancelled) {
throw new SubscriptionAlreadyCancelled($this->id);
}
$this->status = SubscriptionStatus::Cancelled;
$this->cancelledAt = $now;
$this->raise(new SubscriptionCancelled($this->id, $now));
}
}Every design decision here is deliberate:
- Value Objects (
SubscriptionId,UserId,Plan) replace primitive strings: no more passing a user ID where a subscription ID is expected - Private constructor + named factory (
start) enforces valid creation: you can't instantiate a subscription without going through the domain logic - Behavioral methods (
cancel) replace setters: the entity protects its own invariants - Domain Events (
SubscriptionStarted,SubscriptionCancelled) make side effects explicit and decoupled
Bounded Context Awareness
The plugin doesn't just generate individual classes. The /craftsman:design command walks through a 4-phase process:
- Understand: what is the ubiquitous language? What are the invariants?
- Challenge: is this really an Aggregate Root, or should it be a Value Object? Are we crossing context boundaries?
- Recommend: propose the tactical pattern with trade-offs explained
- Implement: scaffold with tests, following the Iron Law
When you ask Claude to add a Payment to the Subscription aggregate, the plugin challenges: "Should Payment belong to the Subscription bounded context, or does it belong in a Billing context with its own aggregate root?" This is the kind of question a senior architect asks. Now your AI asks it too.
Value Objects: The Foundation
The craftsman approach starts from the bottom. Before building entities, you build Value Objects:
// Branded type: compile-time safety, zero runtime cost
type SubscriptionId = string & { readonly __brand: 'SubscriptionId' };
function createSubscriptionId(value: string): SubscriptionId {
if (!value.startsWith('sub_')) {
throw new InvalidSubscriptionId(value);
}
return value as SubscriptionId;
}Branded types in TypeScript, Value Objects in PHP: the pattern adapts to the language, but the principle is constant: domain primitives carry meaning and validation. A string is just bytes. A SubscriptionId is a contract.
The Forge Shapes the Steel
DDD without discipline is just expensive naming. AI without DDD is just fast typing. The craftsman's forge combines both: Claude's generation speed with the structural rigor of tactical patterns.
Your domain model is the most important code in your system. It deserves more than getString/setString.