Architecture Overview
SaaS4Builders uses a domain-driven, layered architecture on the backend and a vertical-slice architecture on the frontend. This page gives you the mental model you need to know where your code goes and how requests flow through the system.
The 30-Second Mental Model
Every API request follows the same pipeline:
HTTP → Request → Input DTO → Action / Query → Domain → Model → Resource → JSON
Four principles govern the flow:
- Controllers delegate — they contain no business logic
- Actions mutate — wrapped in a database transaction
- Queries read — no side effects, ever
- Resources define the API contract — stable, versioned, snake_case output
If you remember nothing else, remember this: controllers receive, actions do, resources respond.
Layer Diagram
The backend is organized into distinct layers, each with a clear responsibility:
backend/app/
├── Application/ # Use cases (Actions, Queries, Input DTOs)
├── Domain/ # Pure business logic (Enums, ValueObjects, Contracts, Rules)
├── Infrastructure/ # External integrations (Providers, Mappers)
├── Http/ # API layer (Controllers, Requests, Resources)
├── Models/ # Eloquent models
├── Policies/ # Authorization policies
└── Support/ # Helpers, traits, cross-cutting concerns
Each layer has strict dependency rules:
- Domain depends on nothing (no Laravel, no Eloquent, no HTTP)
- Application depends on Domain (uses contracts, enums, value objects)
- Infrastructure depends on Domain (implements its contracts)
- Http depends on Application (calls actions and queries)
How a Request Flows Through the System
Let's trace a real request — creating a subscription via Stripe Checkout — through every layer.
Step 1: HTTP Validation and DTO Mapping
The request arrives at a FormRequest. It validates the input, authorizes the user, and maps the validated data to an Input DTO via the toDto() method:
final class CreateCheckoutRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Authorization handled by tenant.member middleware
}
public function rules(): array
{
return [
'plan_id' => ['required', 'uuid', 'exists:plans,id'],
'currency' => ['required', 'string', 'size:3', 'exists:currencies,code'],
'success_url' => ['required', 'url'],
'cancel_url' => ['required', 'url'],
'quantity' => ['nullable', 'integer', 'min:1'],
];
}
public function toDto(Tenant $tenant): CreateSubscriptionData
{
return new CreateSubscriptionData(
tenant: $tenant,
plan: Plan::findOrFail($this->validated('plan_id')),
currency: $this->validated('currency'),
successUrl: $this->validated('success_url'),
cancelUrl: $this->validated('cancel_url'),
quantity: $this->integer('quantity', 1),
);
}
}
Every FormRequest must implement toDto(). This is the only place where HTTP input is mapped to application-level data.
Step 2: Action Executes Business Logic
The controller passes the DTO to an Action. The Action orchestrates business rules, calls Domain services, and delegates external operations to contracts (interfaces):
final class CreateSubscription
{
public function __construct(
private PaymentGatewayInterface $gateway, // Domain contract, not Stripe
private BillingReadinessChecker $checker,
) {}
public function viaCheckout(CreateSubscriptionData $data): CheckoutSession
{
// 1. Validate business rules (readiness, configuration, uniqueness)
// 2. Resolve PlanPrice for the requested currency
// 3. Normalize seat quantity for seat-based plans
// 4. Delegate to the payment gateway via the Domain contract
return $this->gateway->createCheckoutSession($checkoutData);
}
}
The Action depends on PaymentGatewayInterface (a Domain contract), not on StripePaymentGateway directly. This is how the architecture stays provider-agnostic. See the full Action code for all validation steps.
Step 3: Infrastructure Handles External Calls
The StripePaymentGateway implements PaymentGatewayInterface. It calls the Stripe SDK and returns Domain DTOs — the Application layer never sees Stripe-specific types.
Step 4: Controller Returns the Response
The controller wraps the result in a Resource and returns JSON:
public function show(string $tenantId, GetSubscription $query): JsonResponse
{
$subscription = $query->execute($tenantId);
if ($subscription === null) {
return response()->json(['data' => null]);
}
return response()->json([
'data' => new SubscriptionResource($subscription),
]);
}
Controllers are thin. They receive the request, call an Action or Query, and return a Resource. That is all.
Full Flow Diagram
Where Does My Code Go?
| I want to... | I put it in... | Example |
|---|---|---|
| Create, update, or delete data | Application/<Domain>/Actions/ | CreateSubscription |
| List, filter, search, or paginate | Application/<Domain>/Queries/ | ListInvoices |
| Define input data for a use case | Application/<Domain>/DTO/ | CreateSubscriptionData |
| Validate HTTP input | Http/Requests/Api/V1/<Context>/ | CreateCheckoutRequest |
| Format API response | Http/Resources/Api/V1/<Context>/ | SubscriptionResource |
| Define a business contract (interface) | Domain/<Domain>/Contracts/ | PaymentGatewayInterface |
| Create an enum or value object | Domain/<Domain>/Enums/ or ValueObjects/ | PricingType, Money |
| Write a reusable business rule | Domain/<Domain>/Rules/ | CanAccessFeature |
| Integrate an external service | Infrastructure/<Domain>/Providers/<Provider>/ | StripePaymentGateway |
| Map external data to domain types | Infrastructure/<Domain>/Mappers/ | TaxBreakdownMapper |
| Write feature tests | tests/Feature/Api/V1/... | SubscriptionTest |
| Write unit tests | tests/Unit/... | CreateSubscriptionTest |
Action vs Query
| Question | Action | Query |
|---|---|---|
| Mutates state? | Yes | No |
Uses DB::transaction()? | Yes | No |
| Returns model? | Usually | Sometimes (paginator, collection) |
| Side effects allowed? | Yes (events, external calls) | Never |
| Naming convention | CreateX, CancelX, UpdateX | ListX, FindX, GetX |
The rule: if it changes anything — database, external service, file system — it is an Action.
Frontend Architecture at a Glance
The frontend uses a vertical-slice architecture. Each feature is a self-contained module with its own types, schemas, API client, composables, components, stores, and pages:
frontend/features/
├── foundation/ # Auth, tenancy, settings — always loaded
├── core/ # Billing, team, usage — core SaaS features
└── product/ # Your custom product features go here
What's Next
Dive into each backend layer:
- Domain Layer — Pure business logic with zero framework dependencies
- Application Layer — Use case orchestration with Actions and Queries
- Infrastructure Layer — External integrations and provider implementations
Deployment Guide
Production checklist and deployment strategies for SaaS4Builders on Forge, Ploi, or Docker-based infrastructure.
Domain Layer
Framework-agnostic business logic: contracts, DTOs, enums, value objects, domain rules, services, and exceptions - everything that defines how your SaaS actually works.