Domain Best Practices
Architecture decision
Domain modeling and modular boundaries follow the modular monolith approach described in ADR 0001: Modular monolith architecture.
1. Single Responsibility
Each domain should have one clear purpose:
php
// GOOD - Vendor domain handles vendor-related logic
VendorService::approveVendor($vendor);
VendorService::suspendVendor($vendor);
// BAD - Vendor domain handling unrelated concerns
VendorService::sendMarketingEmail($vendor); // This belongs in Communications2. Minimize Cross-Domain Dependencies
Prefer events over direct service calls:
php
// GOOD - Event-driven
event(new BookingCreated($booking));
// ACCEPTABLE - Service contract
$paymentService->charge($amount);
// AVOID - Direct tight coupling
$communicationsModule->sendEmail($data);3. Domain Language
Use ubiquitous language from the business domain:
php
// GOOD - Domain language
$vendor->approve();
$listing->feature();
$booking->confirm();
// BAD - Technical language
$vendor->setStatusApproved();
$listing->updateFeaturedFlag(true);
$booking->changeStateToConfirmed();4. Keep Core Logic in Services
Controllers should be thin, services should contain business logic:
php
// Controller
public function store(CreateVendorRequest $request, VendorService $service): JsonResponse
{
$vendor = $service->createVendor($request->validated());
return response()->json(new VendorResource($vendor), 201);
}
// Service
public function createVendor(array $data): Vendor
{
$vendor = Vendor::create($data);
$this->notifyAdministrators($vendor);
$this->setupDefaultSettings($vendor);
return $vendor;
}5. Test at Domain Boundaries
Test each domain independently:
php
// Test domain behavior
test('can approve vendor', function () {
$vendor = Vendor::factory()->create(['delivery_status' => 'pending']);
$service = app(VendorService::class);
$service->approve($vendor);
expect($vendor->refresh()->delivery_status)->toBe('approved');
});Anti-Patterns to Avoid
1. God Modules
Don't create modules that do everything:
php
// BAD - Module doing too much
modules/Core/ // Avoid catch-all modules2. Anemic Domain Models
Models should contain domain logic, not just getters/setters:
php
// BAD - Anemic model
class Vendor extends Model {
// Just properties, no behavior
}
// GOOD - Rich domain model
class Vendor extends Model {
public function approve(): void {
$this->status = VendorStatus::Approved;
$this->approved_at = now();
$this->save();
event(new VendorApproved($this));
}
}3. Shared Database Tables
Each domain should own its tables:
php
// BAD - Multiple domains sharing a table
Vendor::query()->join('communications_messages') // Cross-domain join
// GOOD - Each domain owns its data
event(new VendorCreated($vendor)); // Communications domain handles its own data