Skip to content

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 Communications

2. 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 modules

2. 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

Wedissimo API Documentation