Skip to content

Domain-Driven Design

The starter follows Domain-Driven Design (DDD) principles to organize code around business domains rather than technical layers.

  • Directoryapp
    • DirectoryConsole
      • DirectoryCommands/
        • …
    • DirectoryDomains
      • DirectoryAccess
        • DirectoryEnums/
          • …
        • DirectoryHttp/
          • …
        • DirectoryModels/
          • …
        • DirectoryPolicies/
          • …
        • DirectorySeeders/
          • …
      • DirectoryApi
        • DirectoryActions/
          • …
        • DirectoryEnums/
          • …
        • DirectoryHttp/
          • …
        • DirectoryModels/
          • …
        • DirectoryPassport/
          • …
        • …
      • DirectoryAuth
        • DirectoryActions/
          • …
        • DirectoryEnums/
          • …
        • DirectoryHttp/
          • …
        • DirectoryModels/
          • …
        • …
      • DirectoryCore
        • DirectoryCasts/
          • …
        • DirectoryData/
          • …
        • DirectoryEnums/
          • …
        • DirectoryExceptions/
          • …
        • DirectoryHealth/
          • …
        • DirectoryModels/
          • …
        • DirectoryServices/
          • …
        • …
      • DirectorySupport
        • DirectoryActions/
          • …
        • DirectoryEnums/
          • …
        • DirectoryGateways/
          • …
        • DirectoryModels/
          • …
        • DirectoryRepositories/
          • …
        • DirectorySeeders/
          • …
        • …
      • DirectoryUser
        • DirectoryActions/
          • …
        • DirectoryEnums/
          • …
        • DirectoryModels/
          • …
        • DirectoryPolicies/
          • …
        • DirectoryQueryBuilders/
          • …
        • …
    • DirectoryFilament
      • DirectoryAdministration/ the administration panel
        • DirectoryClusters/
          • …
        • DirectoryPages/
          • …
        • DirectoryResources/
          • …
        • …
      • DirectoryApp/ the app panel
        • DirectoryPages/
          • …
        • DirectoryStarter/Pages/
          • …
        • …
      • DirectorySupport/ helpers both panels use
        • …
    • DirectoryHttp/
      • DirectoryControllers/
        • …
      • DirectoryLivewire/
        • …
      • DirectoryMiddleware/
        • …
    • DirectoryMcp/ the MCP server and its tools
      • …
    • DirectoryProviders/
      • …
    • DirectoryView/
      • DirectoryComponents/
        • …

The Core domain contains foundational functionality that applies across the entire application:

Location: app/Domains/Core/

Responsibilities:

  • BaseModel, which models extend. Model concerns shared with other applications, such as PrunesAfterRetentionPeriod for records that expire and RecordsCustomAudits for audit events a model’s own changes don’t capture (which the starter’s RecordsAuditEvents builds on), come from Chassis
  • The typed preferences base class (Data/Preferences), which UserPreferences extends
  • The audit log: the Audit model, the AuditEvent enum that names every event, and RecordsAuditEvents for recording one
  • Filament’s table Export model, which prunes exports and their files after platform.retention.exports days
  • The ExternalService enum
  • Configuration validators (Services/ConfigValidation/)
  • Shared exceptions (NoRollbackException, ServiceDownError, SentryExceptionHandler)
  • Custom health checks (Health/)
  • Shared casts (MarkdownWithJiraLinksCast)

When to add code to Core:

  • Functionality needed by multiple domains
  • Infrastructure concerns (logging, caching, etc.)
  • Shared enums
  • Base classes and traits
  • Framework extensions

The Auth domain handles who someone is: signing in and out.

Location: app/Domains/Auth/

Responsibilities:

  • The sign-in methods and the step every sign-in ends with (SignIn), and single sign-on through Online Passport or Entra ID
  • Email login codes (LoginCodes)
  • Sign-in-as for seeded users in the local environment
  • Impersonation and its log

The Access domain handles what someone may do: roles and permissions.

Location: app/Domains/Access/

Responsibilities:

  • SystemPermission, the single source of truth for permissions, and the system roles
  • Roles, role types and permissions, and their seeding
  • Recording changes to roles and permissions (AuditsRoles, AuditsPermissions), and finding which role grants a permission (TracksPermissionSources)

The Api domain handles the credentials that call the application:

Location: app/Domains/Api/

Responsibilities:

  • Who may see, issue, change, revoke or use each credential (CredentialAccess), and every OAuth scope (ApiScopes)
  • Service clients, personal access tokens, OAuth applications and the connections people make to them
  • The OAuth consent screen
  • The API’s middleware: authenticating tokens, scopes, rate limits and request logging

The User domain handles everything related to user data and management:

Location: app/Domains/User/

Responsibilities:

  • User management and profile data
  • User segmentation and affiliation
  • Directory integration (searching, syncing)
  • User login tracking
  • Wildcard photo management

The Support domain handles the changelog, support tickets and announcements:

Location: app/Domains/Support/

Responsibilities:

  • Changelog entries, synced from resources/changelogs/ by ChangelogSeeder
  • Support tickets submitted from the Contact Support page
  • Ticket gateways (TeamDynamix and mail), with a fallback to mail when the primary gateway fails
  • Announcements: publishing, audiences, dismissals and notifications

Purpose: Encapsulate single-responsibility business operations

Location: app/Domains/{Domain}/Actions/

Characteristics:

  • Single method, __invoke(), usually on a readonly class
  • Focused on one specific operation
  • Testable in isolation
  • Resolved from the container: injected into controllers, other actions and Filament action closures, or called with resolve(SomeAction::class)(...)
  • The rules belong inside: an action that issues, changes or revokes a credential takes the person acting, asks App\Domains\Api\CredentialAccess whether they may, and records its audit event when someone else’s credential changes, so every caller gets the same behavior (CreatePersonalAccessToken, DisconnectApplication, RegisterOAuthApplication). Only seeders and scheduled sweeps pass no one

Example:

namespace App\Domains\User\Actions;
class DetermineUserSegment
{
public function __invoke(User $user): UserSegment
{
// Business logic to determine user segment
}
}

When to use Actions:

  • Complex business logic that doesn’t belong in a model
  • Operations that span multiple models
  • Reusable operations called from multiple places
  • Operations that need to be tested in isolation

Purpose: Define sets of related constants with behavior

Location: app/Domains/{Domain}/Enums/

Characteristics:

  • Backed by strings or integers
  • Can have methods for labels, descriptions, behavior
  • Type-safe throughout the application

Example:

namespace App\Domains\Auth\Enums;
use Filament\Support\Contracts\HasLabel;
enum AuthType: string implements HasLabel
{
case SSO = 'sso';
case Local = 'local';
case API = 'api';
public function getLabel(): string
{
return match ($this) {
self::SSO => 'NetID',
self::Local => 'Verification Code',
self::API => 'API',
};
}
}

Purpose: Represent database tables and their relationships

Location: app/Domains/{Domain}/Models/

Characteristics:

  • Extend BaseModel, except where a package needs to own the parent class: User extends Authenticatable, Role and Permission extend Spatie’s, and OAuthClient and OAuthToken extend Passport’s. ApiRequestLog extends Eloquent’s Model directly, so the request log isn’t audited.
  • Include relationships, scopes, and accessors

Base Model:

All models should extend BaseModel which provides:

  • Automatic audit logging
  • Shared scopes and utilities

Purpose: Complex operations or integrations with external systems

Location: app/Domains/{Domain}/Services/

Characteristics:

  • More complex than Actions
  • Often stateful
  • Handle external integrations
  • May coordinate multiple Actions

When to use Services:

  • External API integrations
  • Complex calculations or transformations
  • Operations requiring significant setup

Purpose: Encapsulate complex queries and scopes

Location: app/Domains/{Domain}/QueryBuilders/

Characteristics:

  • Extend Eloquent’s query builder
  • Provide fluent, chainable query methods
  • Keep query logic out of controllers or models

Example:

namespace App\Domains\User\QueryBuilders;
use App\Domains\Auth\Enums\AuthType;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModel of User
*
* @extends Builder<TModel>
*/
class UserBuilder extends Builder
{
public function sso(): self
{
return $this->where('auth_type', AuthType::SSO);
}
public function whereEmailEquals(string $email): self
{
$normalized = strtolower(trim($email));
return $this->where('email', 'ilike', $normalized);
}
}

As your application grows, you may want to add new domains. For example, if building a course management system, you might add:

  • Directoryapp/Domains/Course/
    • DirectoryActions/
      • EnrollStudent.php
      • UnenrollStudent.php
    • DirectoryEnums/
      • CourseStatus.php
    • DirectoryModels/
      • Course.php
      • Enrollment.php
      • Section.php
    • DirectoryServices/
      • CourseRegistrationService.php

Guidelines for new domains:

  1. Domain should represent a business concept - Not a technical layer
  2. Domain should have clear boundaries - Minimal coupling with other domains
  3. Domain should be cohesive - All code in domain relates to same concept