The Folder Structure That Scales
Series: Every Laravel Project Should Have These Building Blocks
Part: 1 of 35 | Level: Beginner | Prerequisites: Introduction
What You’ll Learn
- Why the default Laravel structure breaks down after ~20 routes
- The folder layout used in production apps
- What belongs in each folder and why
- The special role of
app/Support/as your app’s internal library - Common mistakes developers make with folder structure
The Problem with the Default Structure
When you run php artisan make:controller ProductController, Laravel creates a single app/Http/Controllers/ folder. That’s fine for a todo app.
In production, a real application might have 50+ controllers, 30+ services, 20+ jobs, and 15+ model traits. If everything lives in one flat folder, you spend more time searching for files than writing code.
Worse: when everything is in the same place, it’s tempting to put everything in the same class. Controllers grow fat. Models grow logic. Tests become impossible.
The structure in this article forces good habits by making the right place obvious.
The Full Folder Layout
Here is the complete app/ structure used across the projects in this series. We’ll explain each folder in detail below.
app/
├── Actions/
│ └── Order/
│ ├── CreateOrderAction.php
│ └── CancelOrderAction.php
│
├── Console/
│ └── Commands/
│ ├── HeartbeatCommand.php
│ └── PruneOldLogsCommand.php
│
├── DTOs/
│ ├── OrderCustomerData.php
│ └── ServiceCheckResult.php
│
├── Enums/
│ ├── CacheKeys.php
│ ├── PaymentStatus.php
│ └── ServiceStatus.php
│
├── Events/
│ └── OrderPlaced.php
│
├── Exceptions/
│ └── InsufficientStockException.php
│
├── Exports/
│ ├── BaseExport.php
│ └── OrdersExport.php
│
├── Http/
│ ├── Controllers/
│ │ ├── Admin/
│ │ │ └── DashboardController.php
│ │ └── Api/
│ │ └── V1/
│ │ └── OrderController.php
│ ├── Middleware/
│ │ ├── RequestLogger.php
│ │ └── SecureHeadersMiddleware.php
│ └── Requests/
│ ├── StoreOrderRequest.php
│ └── UpdateOrderRequest.php
│
├── Jobs/
│ ├── HeartBeat.php
│ └── ProcessPaymentJob.php
│
├── Listeners/
│ └── SendOrderConfirmation.php
│
├── Mail/
│ ├── ExceptionOccurred.php
│ └── OrderConfirmationMail.php
│
├── Models/
│ ├── Order.php
│ └── User.php
│
├── Notifications/
│ └── OrderShippedNotification.php
│
├── Observers/
│ └── OrderObserver.php
│
├── Policies/
│ └── OrderPolicy.php
│
├── Providers/
│ └── AppServiceProvider.php
│
├── Rules/
│ └── ValidCouponCode.php
│
├── Services/
│ ├── Core/
│ │ └── ErrorReporter.php
│ ├── Status/
│ │ ├── StatusService.php
│ │ ├── Contracts/
│ │ │ └── StatusServiceInterface.php
│ │ └── Checks/
│ │ ├── DatabaseCheck.php
│ │ └── CacheCheck.php
│ └── Order/
│ ├── OrderService.php
│ └── OrderPaymentService.php
│
└── Support/
├── Contracts/
│ └── IsSluggable.php
├── Traits/
│ ├── Console/
│ │ └── LogsCommandMessages.php
│ ├── ModelChangeLogger.php
│ ├── Sluggable.php
│ └── InteractsWithMedia.php
└── File.php
Folder-by-Folder Breakdown
app/Actions/
What goes here: Single-purpose classes that perform one specific operation.
An Action is different from a Service. A Service is a collaborator you inject and call multiple times. An Action is executed once to complete one atomic operation.
// app/Actions/Order/CreateOrderAction.php
final readonly class CreateOrderAction
{
public function __construct(
private HostingPlanPricingService $pricingService
) {}
public function execute(User $user, OrderCustomerData $data, Collection $cartItems): Order
{
return DB::transaction(function () use ($user, $data, $cartItems): Order {
$order = Order::create([...]);
$this->attachOrderItems($order, $cartItems);
return $order;
});
}
}
Group actions in subdirectories when you have more than 3 related to the same domain: Actions/Order/, Actions/ServiceRequest/, Actions/Certificate/.
app/DTOs/
What goes here: Immutable data transfer objects — typed containers for passing data between layers.
DTOs replace raw arrays. Instead of passing $data['first_name'] and hoping the key exists, you pass a OrderCustomerData object with typed properties.
// app/DTOs/OrderCustomerData.php
final readonly class OrderCustomerData
{
public function __construct(
public string $firstName,
public string $lastName,
public string $email,
public PaymentGateway $paymentGateway,
public ?string $address = null,
) {}
public static function fromArray(array $data): self
{
return new self(
firstName: (string) ($data['first_name'] ?? ''),
lastName: (string) ($data['last_name'] ?? ''),
email: (string) ($data['email'] ?? ''),
paymentGateway: PaymentGateway::tryFrom($data['payment_gateway'] ?? '') ?? PaymentGateway::Unknown,
);
}
}
The readonly modifier (PHP 8.1+) makes properties unmodifiable after construction — perfect for data you want to pass around without mutation.
app/Enums/
What goes here: Typed constants that represent a fixed set of values.
Enums replace magic strings like 'status' => 'pending'. They give you IDE autocompletion, static analysis type checking, and the ability to attach behavior to values.
// app/Enums/CacheKeys.php
enum CacheKeys: string
{
case DashboardOrders = 'admin.dashboard.orders';
case DashboardClaims = 'admin.dashboard.claims';
public function forYear(int $year): string
{
return sprintf('%s.%d', $this->value, $year);
}
}
// Usage
Cache::flexible(CacheKeys::DashboardOrders->value, [1800, 3600], fn () => ...);
Cache::flexible(CacheKeys::DashboardOrders->forYear(2024), [1800, 3600], fn () => ...);
app/Services/
What goes here: Stateless domain logic that doesn’t fit in a single Action — things that query, compute, coordinate, or integrate with external APIs.
Organize services in subdirectories by domain: Services/Order/, Services/Status/, Services/Core/. The Services/Core/ directory is special — it holds infrastructure services like ErrorReporter that cut across the entire application.
Services can have contracts (interfaces). When they do, bind the interface to the implementation in AppServiceProvider.
// app/Services/Order/OrderService.php
final class OrderService
{
public function __construct(
private readonly StripePaymentServiceInterface $stripe,
) {}
public function processPayment(Order $order, string $paymentIntentId): void
{
// coordinate between Stripe and the order
}
}
app/Support/
What goes here: Shared utilities, reusable traits, and contracts that are used across multiple parts of the application.
This is the most important folder most developers skip. Think of app/Support/ as a private package that lives inside your app. It’s where you put things that don’t belong to any one domain but are used by many.
app/Support/
├── Contracts/ # Interfaces that define behaviour
│ └── IsSluggable.php
├── Traits/
│ ├── Console/
│ │ └── LogsCommandMessages.php # Dual console+log output
│ ├── ModelChangeLogger.php # Audit trail for any model
│ ├── Sluggable.php # Auto-generate URL slugs
│ ├── HasAddress.php # Address fields mixin
│ └── InteractsWithMedia.php # Media upload/retrieval
└── File.php # File utility: hash, MIME, safe names
The key distinction: Support/ is for infrastructure and cross-cutting concerns. If you find yourself writing the same trait for a second model, move it to Support/Traits/.
app/Http/Controllers/
What goes here: HTTP handlers — nothing more.
A controller’s job is: receive a request → validate → authorize → delegate to a service or action → return a response. That’s it. No business logic, no database queries, no loops.
Group controllers in subdirectories by role or domain: Controllers/Admin/, Controllers/Api/V1/, Controllers/Domain/.
app/Http/Requests/
What goes here: Form Requests that handle both validation (rules()) and authorization (authorize()).
Every form submission or API write operation should have its own Form Request. Group them to match their controllers.
app/Observers/
What goes here: Eloquent event hooks for model lifecycle events (created, updated, deleted, etc.) that need to trigger side effects.
Observers are better than putting logic in model boot methods because they’re in a separate class, they’re testable, and they don’t bloat your model file.
app/Policies/
What goes here: Authorization logic — who is allowed to do what to which resource.
Every Model that a user can act on should have a Policy. Register policies in AppServiceProvider or let Laravel auto-discover them.
Common Mistakes to Avoid
Mistake 1: Putting everything in one flat folder. Controllers/ProductController.php, Controllers/UserController.php, Controllers/AdminDashboardController.php — all in one folder with 40 files. Use subdirectories.
Mistake 2: Skipping app/Support/. When you need the same trait in three models, you copy-paste it. Then you fix a bug in one copy and forget the other two. That’s what Support/Traits/ is for.
Mistake 3: Using app/Helpers/ as a dumping ground. A helpers.php file with 50 functions is a god-file. Every function in it should either be a method on the appropriate class, or a static method on an appropriate support class.
Mistake 4: Not grouping services by domain. Services/CreateUserService.php, Services/UpdateUserService.php, Services/DeleteUserService.php — three files for one domain. Use Services/User/ as a subdirectory.
Key Takeaways
- The folder structure forces good habits. When there’s an obvious right place for code, you’re less likely to put it in the wrong place.
app/Support/is your internal library. Use it to share code across domains without coupling domains to each other.- Group by domain within each folder when you have more than 3–4 related files.
- Controllers belong in
Http/Controllers/only if they handle HTTP. Don’t let them grow past their single responsibility.
Tips and Gotchas
⚠️ Warning: Don’t over-engineer structure before your first feature. Start with the layout above, move things as they grow. Premature domain splitting leads to empty folders and awkward imports.
💡 Tip: If you’re unsure where a class belongs — “Is this HTTP-specific?” →
Http/. “One task?” →Actions/. “Spans multiple models?” →Services/. “Shared utility?” →Support/.
🔥 Expert Note:
app/Support/is your internallib/folder. It’s code everything depends on but that doesn’t own any domain. Keep it separate to prevent circular dependencies.
Further Reading
- Laravel Docs: Directory Structure
- Laravel Docs: Service Container
- Spatie: Laravel Guidelines — conventions from one of the most prolific Laravel teams
4 Comments