faran / pulsar
An opinionated Laravel architecture for building modular, domain-driven applications at scale
Installs: 2
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/faran/pulsar
Requires
- php: ^8.2
- symfony/console: ^7.0
Requires (Dev)
- pestphp/pest: ^4.3
README
An opinionated Laravel architecture for building modular, domain-driven applications at scale.
Pulsar is an opinionated architecture tool. It provides a specific approach to organizing Laravel applications using clean architecture, domain-driven design, and service-oriented patterns. This architecture works well for medium-to-large scale applications, multi-tenant SaaS platforms, and teams that benefit from explicit boundaries between business logic and delivery mechanisms. If you prefer Laravel's default structure or other architectural patterns, Pulsar may not be the right fit for your project.
Table of Contents
- Installation
- Architecture
- File Types
- Commands Reference
- Complete Examples
- Best Practices
- Contributing
Installation
composer require faran/pulsar --dev
Architecture
Pulsar organizes your Laravel application into two complementary layers, following clean architecture and domain-driven design principles. Each feature is organized as a vertical slice through the Service Layer, while shared business logic lives in the Domain Layer.
Service Layer
The Service Layer handles HTTP delivery and application orchestration. Each Service represents an API delivery boundary scoped to a specific consumer audience (e.g., Admin, Client), not a business capability. Business logic lives in the shared Domain layer.
Structure:
app/Services/{Service}/
├── Providers/
│ ├── {Service}ServiceProvider.php # Bootstraps the service
│ └── RouteServiceProvider.php # Registers routes
├── Routes/
│ └── api.php # API routes (/api/{service-slug}/*)
└── Modules/{Module}/
├── Controllers/ # HTTP request handlers
├── Requests/ # Input validation
├── UseCases/ # Application logic
└── Operations/ # Reusable action/query sequences
Example: Admin API Service
app/Services/Admin/
├── Providers/
├── Routes/api.php
└── Modules/
├── Orders/
│ ├── Controllers/OrderController.php
│ ├── Requests/UpdateOrderRequest.php
│ └── UseCases/ManageOrder.php
└── Products/
├── Controllers/ProductController.php
├── Requests/CreateProductRequest.php
└── UseCases/CreateProduct.php
Routes: /api/admin/orders, /api/admin/products
The same Domain models (Order, Product) are used by both Admin and Client services — each service exposes different endpoints, validation, and response shapes for its audience.
Domain Layer
The Domain Layer contains pure business logic independent of HTTP, frameworks, or infrastructure. Organized by business domain.
Structure:
app/Domain/{Domain}/
├── Models/ # Eloquent models
├── Actions/ # Business operations
├── DTOs/ # Data transfer objects
├── Policies/ # Authorization rules
├── Events/ # Domain events
├── Enums/ # Domain states
├── Exceptions/ # Business rule violations
└── Queries/ # Complex read operations
Example: E-commerce Product Domain
app/Domain/Product/
├── Models/Product.php
├── Actions/UpdateStockAction.php
├── DTOs/ProductData.php
├── Policies/ProductPolicy.php
├── Events/ProductOutOfStock.php
├── Enums/ProductStatus.php
├── Exceptions/InsufficientStockException.php
└── Queries/GetProductsByCategory.php
composer require faran/pulsar --dev
Generate Your First Service
pulsar make:service Admin
Then follow the sections below to generate individual file types.
File Types
💡 Naming Freedom: Pulsar gives you complete control over class names. Examples below use suffixes like
Controller,Action,UseCasefor clarity, but you can name classes however you prefer:
pulsar make:controller ProductController ...→ProductController.php✅pulsar make:controller Product ...→Product.php✅pulsar make:action CreateOrderAction ...→CreateOrderAction.php✅pulsar make:action CreateOrder ...→CreateOrder.php✅The generated class name matches exactly what you specify.
Service Layer Files
Controllers
Purpose: Handle HTTP requests and orchestrate application flow.
Command:
pulsar make:controller ProductController Products Admin
Location: app/Services/{Service}/Modules/{Module}/Controllers/
Example:
class ProductController extends Controller { public function __construct( private ListProductsUseCase $listProducts ) {} public function index(ListProductsRequest $request): JsonResponse { $products = $this->listProducts->execute($request->validated()); return response()->json($products); } }
Requests
Purpose: Validate input and authorize requests.
Command:
pulsar make:request AddToCartRequest Cart Client
Location: app/Services/{Service}/Modules/{Module}/Requests/
Example:
class AddToCartRequest extends FormRequest { public function rules(): array { return [ 'product_id' => 'required|exists:products,id', 'quantity' => 'required|integer|min:1', ]; } }
UseCases
Purpose: Application-specific business logic coordinating domain operations.
Command:
pulsar make:use-case PlaceOrder Orders Client
Location: app/Services/{Service}/Modules/{Module}/UseCases/
Example:
class PlaceOrderUseCase { public function __construct( private CreateOrderAction $createOrder, private UpdateStockAction $updateStock, ) {} public function execute(OrderData $data): Order { return DB::transaction(function () use ($data) { $order = $this->createOrder->execute($data); foreach ($data->items as $item) { $this->updateStock->execute($item['product_id'], -$item['quantity']); } event(new OrderPlaced($order)); return $order; }); } }
Operations
Purpose: Reusable sequences of Actions/Queries that can be shared across multiple UseCases.
When multiple UseCases need the same sequence of Actions/Queries, extract it into an Operation.
Command:
pulsar make:operation SaveAddress Account Client
Location: app/Services/{Service}/Modules/{Module}/Operations/
Example:
class SaveAddressOperation { public function __construct( private FormatAddressAction $formatAddress, private RemoveNullFieldsAction $removeNullFields, private SaveAddressAction $saveAddress, ) {} public function execute(array $addressData, User $user): Address { // 1. Format address (standardize formatting) $formatted = $this->formatAddress->execute($addressData); // 2. Remove null fields (clean data) $cleaned = $this->removeNullFields->execute($formatted); // 3. Save address return $this->saveAddress->execute($cleaned, $user); } }
When to use Operations:
- Multiple UseCases perform the same sequence of Actions/Queries
- You need to reuse a multi-step process across different workflows
- A group of Actions/Queries always execute together
When to use UseCases instead:
- Single workflow specific to one feature (not reused elsewhere)
- Workflow owns transaction boundaries
- Workflow emits domain events
Domain Layer Files
Models
Purpose: Eloquent models representing domain entities.
Command:
pulsar make:model Product Catalog
Location: app/Domain/{Domain}/Models/
Example:
class Product extends Model { protected $fillable = ['name', 'price', 'stock']; public function isInStock(): bool { return $this->stock > 0; } }
Actions
Purpose: Atomic business operations encapsulating domain logic.
Command:
pulsar make:action UpdateProductStock Catalog
Location: app/Domain/{Domain}/Actions/
Example:
class UpdateProductStock { public function execute(Product $product, int $quantity): Product { if ($product->stock + $quantity < 0) { throw new InsufficientStockException($product); } $product->update(['stock' => $product->stock + $quantity]); if ($product->stock === 0) { event(new ProductOutOfStock($product)); } return $product->fresh(); } }
DTOs
Purpose: Immutable data carriers for transferring data between layers.
Command:
pulsar make:dto OrderData Order
Location: app/Domain/{Domain}/DTOs/
Example:
readonly class OrderData { public function __construct( public int $customerId, public array $items, public string $shippingAddress, ) {} public static function from(array $data): self { return new self( customerId: $data['customer_id'], items: $data['items'], shippingAddress: $data['shipping_address'], ); } }
Policies
Purpose: Business authorization rules for domain entities.
Command:
pulsar make:policy OrderPolicy Order
Location: app/Domain/{Domain}/Policies/
Example:
class OrderPolicy { public function canCancel(User $user, Order $order): bool { return $order->status === OrderStatus::PENDING && $order->customer_id === $user->id; } }
Events
Purpose: Domain events signaling significant business occurrences.
Command:
pulsar make:event OrderPlaced Order
Location: app/Domain/{Domain}/Events/
Example:
class OrderPlaced { public function __construct( public readonly Order $order, public readonly DateTimeImmutable $occurredAt = new DateTimeImmutable(), ) {} }
Enums
Purpose: Fixed sets of domain values and states.
Command:
pulsar make:enum OrderStatus Order
Location: app/Domain/{Domain}/Enums/
Example:
enum OrderStatus: string { case PENDING = 'pending'; case PROCESSING = 'processing'; case SHIPPED = 'shipped'; case DELIVERED = 'delivered'; case CANCELLED = 'cancelled'; }
Exceptions
Purpose: Domain-specific business rule violations.
Command:
pulsar make:exception InsufficientStockException Catalog
Location: app/Domain/{Domain}/Exceptions/
Example:
class InsufficientStockException extends Exception { public function __construct(Product $product) { parent::__construct("Product {$product->name} has insufficient stock"); } }
Queries
Purpose: Complex read-only domain queries.
Command:
pulsar make:query GetCustomerOrders Order
Location: app/Domain/{Domain}/Queries/
Example:
class GetCustomerOrdersQuery { public function execute(int $customerId): Collection { return Order::where('customer_id', $customerId) ->with('items.product') ->latest() ->get(); } }
Complete Examples
Service Layer Example
Building an Admin API service:
# 1. Create the service pulsar make:service Admin # 2. Create orders module (admin order management) pulsar make:controller OrderController Orders Admin -r pulsar make:request UpdateOrderRequest Orders Admin pulsar make:use-case ManageOrder Orders Admin # 3. Create products module (admin product management) pulsar make:controller ProductController Products Admin -r pulsar make:request CreateProductRequest Products Admin pulsar make:use-case CreateProduct Products Admin pulsar make:operation SyncInventory Products Admin
Resulting structure:
app/Services/Admin/
├── Providers/
│ ├── AdminServiceProvider.php
│ └── RouteServiceProvider.php
├── Routes/
│ └── api.php
└── Modules/
├── Orders/
│ ├── Controllers/OrderController.php
│ ├── Requests/UpdateOrderRequest.php
│ └── UseCases/ManageOrder.php
└── Products/
├── Controllers/ProductController.php
├── Requests/CreateProductRequest.php
├── UseCases/CreateProduct.php
└── Operations/SyncInventory.php
Define routes in app/Services/Admin/Routes/api.php:
use App\Services\Admin\Modules\Orders\Controllers\OrderController; use App\Services\Admin\Modules\Products\Controllers\ProductController; use Illuminate\Support\Facades\Route; Route::apiResource('/orders', OrderController::class); Route::apiResource('/products', ProductController::class);
Routes accessible at: /api/admin/orders, /api/admin/products
Domain Layer Example
Building an e-commerce domain:
# 1. Create domain models pulsar make:model Product Catalog pulsar make:model Order Order # 2. Create actions for business operations pulsar make:action UpdateProductStock Catalog pulsar make:action CreateOrder Order # 3. Create DTOs for data transfer pulsar make:dto ProductData Catalog pulsar make:dto OrderData Order # 4. Create policies for authorization pulsar make:policy ProductPolicy Catalog pulsar make:policy OrderPolicy Order # 5. Create domain events pulsar make:event ProductOutOfStock Catalog pulsar make:event OrderPlaced Order # 6. Create enums for states pulsar make:enum ProductStatus Catalog pulsar make:enum OrderStatus Order # 7. Create domain exceptions pulsar make:exception InsufficientStockException Catalog pulsar make:exception OrderAlreadyCancelledException Order # 8. Create queries for complex reads pulsar make:query GetLowStockProducts Catalog pulsar make:query GetCustomerOrders Order
Resulting structure:
app/Domain/
├── Catalog/
│ ├── Models/Product.php
│ ├── Actions/UpdateProductStock.php
│ ├── DTOs/ProductData.php
│ ├── Policies/ProductPolicy.php
│ ├── Events/ProductOutOfStock.php
│ ├── Enums/ProductStatus.php
│ ├── Exceptions/InsufficientStockException.php
│ └── Queries/GetLowStockProducts.php
└── Order/
├── Models/Order.php
├── Actions/CreateOrder.php
├── DTOs/OrderData.php
├── Policies/OrderPolicy.php
├── Events/OrderPlaced.php
├── Enums/OrderStatus.php
├── Exceptions/OrderAlreadyCancelledException.php
└── Queries/GetCustomerOrders.php
Architecture Best Practices
Layer Responsibilities
Request → Controller → UseCase → Actions/Operations/Queries → Models
↓
Response
Controllers (HTTP Layer)
- Extract validated data from Request
- Call UseCase with DTOs or validated arrays
- Transform domain results to HTTP responses
- Never contain business logic
class OrderController extends Controller { public function __construct( private PlaceOrder $placeOrder ) {} public function store(PlaceOrderRequest $request): JsonResponse { $order = $this->placeOrder->execute( OrderData::from($request->validated()) ); return response()->json($order, 201); } }
Requests (Validation Layer)
- Validate input structure and format
- Authorization checks (via
authorize()method) - No business logic - only input validation
- Business rule validation belongs in Actions/UseCases
class PlaceOrderRequest extends FormRequest { public function authorize(): bool { // Policy-based authorization return $this->user()->can('create', Order::class); } public function rules(): array { // Structure validation only return [ 'customer_id' => 'required|exists:customers,id', 'items' => 'required|array|min:1', 'items.*.product_id' => 'required|exists:products,id', 'items.*.quantity' => 'required|integer|min:1', ]; } }
UseCases (Application Orchestration)
- Orchestrate business workflows
- Coordinate multiple Actions and Operations
- Own database transaction boundaries
- Emit domain events
- Never coupled to HTTP Request objects
class PlaceOrderUseCase { public function __construct( private CreateOrderAction $createOrder, private UpdateStockAction $updateStock, private ReserveInventoryAction $reserveInventory, ) {} public function execute(OrderData $data): Order { return DB::transaction(function () use ($data) { // 1. Reserve inventory (prevents overselling) $this->reserveInventory->execute($data->items); // 2. Create the order $order = $this->createOrder->execute($data); // 3. Decrement stock for each item foreach ($data->items as $item) { $this->updateStock->execute($item['product_id'], -$item['quantity']); } // 4. Emit event for side effects (email, notifications, etc.) event(new OrderPlaced($order)); return $order; }); } }
Actions (Domain Operations)
- Atomic: One action = one domain operation
- Encapsulate business rules
- Validate business invariants
- Can emit domain events
- Return domain objects, booleans, collections, or void
class UpdateStockAction { public function execute(Product $product, int $quantity): Product { // Business rule validation if ($product->stock + $quantity < 0) { throw new InsufficientStockException($product); } // State mutation $product->update(['stock' => $product->stock + $quantity]); // Domain event if ($product->stock === 0) { event(new ProductOutOfStock($product)); } return $product->fresh(); } }
Valid Action Return Types:
- Domain objects:
CreateOrderAction→Order - Collections:
BulkUpdateProductsAction→Collection<Product> - Booleans:
ActivateAccountAction→bool - Void:
SendNotificationOperation→void - Primitives:
CalculateTaxAction→float
Operations (Reusable Action Orchestration)
- Compose Actions/Queries into reusable sequences
- Extract common action chains shared across multiple UseCases
- Can call Actions, Queries, and Models
- Don't call other Operations or UseCases
class SaveAddressOperation { public function __construct( private FormatAddressAction $formatAddress, private RemoveNullFieldsAction $removeNullFields, private SaveAddressAction $saveAddress, ) {} public function execute(array $addressData, User $user): Address { // 1. Format address $formatted = $this->formatAddress->execute($addressData); // 2. Remove null fields $cleaned = $this->removeNullFields->execute($formatted); // 3. Save address return $this->saveAddress->execute($cleaned, $user); } }
Operation Examples:
SaveAddressOperation- Format → Clean → Save address (reused by multiple UseCases)ProcessPaymentOperation- Validate payment → Charge → Record transactionPrepareOrderDataOperation- Validate items → Calculate totals → Apply discountsSyncInventoryOperation- Fetch stock → Update cache → Notify if low
UseCase vs Operation:
// ✅ UseCase: Full feature workflow (owns transactions, emits events) class PlaceOrderUseCase { public function __construct( private SaveAddressOperation $saveAddress, // Reuses Operation ) {} public function execute(OrderData $data): Order { return DB::transaction(function () use ($data) { // UseCase orchestrates the full workflow $address = $this->saveAddress->execute($data->address, $data->user); // ... create order, update stock, etc. event(new OrderPlaced($order)); return $order; }); } } // ✅ Operation: Reusable action sequence (called by multiple UseCases) class SaveAddressOperation { public function execute(array $addressData, User $user): Address { // Operation composes multiple Actions return $this->saveAddress->execute( $this->removeNullFields->execute( $this->formatAddress->execute($addressData) ), $user ); } }
Queries (Read Operations)
- Read-only: Never mutate state
- Complex data retrieval
- Can return primitives, collections, or domain objects
- Optimize for specific read scenarios
class GetCustomerOrdersQuery { public function execute(int $customerId, ?OrderStatus $status = null): Collection { return Order::query() ->where('customer_id', $customerId) ->when($status, fn($q) => $q->where('status', $status)) ->with(['items.product', 'customer']) ->latest() ->get(); } }
Query Examples:
HasActiveSubscriptionQuery→boolGetLowStockProductsQuery→Collection<Product>CalculateCartTotalQuery→floatFindProductsByCategoryQuery→Collection<Product>
Atomicity: Actions and Queries
Actions are atomic — One business operation on one model/aggregate. No calling other Actions or Queries.
Queries are atomic — One read operation, return data. No calling other Queries or Actions.
If you need to compose multiple Actions/Queries: Use an Operation (reusable sequence) or UseCase (full workflow).
Dependency Rules
What Can Call What:
Controllers → UseCases ✅
Controllers → Operations ✅ (for simple cases)
Controllers → Actions ❌ (use UseCase instead)
UseCases → Actions ✅
UseCases → Operations ✅
UseCases → Queries ✅
UseCases → Other UseCases ❌ (extract shared logic to Action)
Actions → Models ✅ (atomic - single operation)
Actions → Other Actions ❌ (compose in UseCase/Operation)
Actions → Queries ❌ (UseCase/Operation passes needed data)
Operations → Actions ✅
Operations → Models ✅
Operations → Other Operations ❌
Operations → UseCases ❌
Queries → Models ✅
Queries → Other Queries ❌
Queries → Actions ❌
Domain Layer Purity:
- Domain layer (Models, Actions, DTOs, Events, Enums, Exceptions, Queries) has ZERO dependencies on Service layer
- Domain is framework-agnostic business logic
- Services consume Domain, never the reverse
Dependency Injection Patterns
Critical for Laravel Octane compatibility and testability:
✅ Constructor = Dependencies, Execute = Data
class PlaceOrderUseCase { // Dependencies in constructor public function __construct( private CreateOrderAction $createOrder, private EmailService $emailService, private LoggerInterface $logger, ) {} // Data in execute method public function execute(OrderData $data): Order { // Implementation } }
❌ Anti-pattern: Data in Constructor
// Don't do this - breaks Octane singleton resolution public function __construct( private OrderData $data, // ❌ State in constructor ) {}
Why This Matters:
- Octane: Classes are singletons - constructor called once, execute() called per request
- Testing: Easy to mock dependencies, easy to test with different data
- Clarity: Clear separation between infrastructure (injected) and data (passed)
Transaction Boundaries
UseCases own transaction boundaries because they understand the complete business workflow:
class PlaceOrderUseCase { public function execute(OrderData $data): Order { return DB::transaction(function () use ($data) { $order = $this->createOrder->execute($data); $this->updateInventory->execute($data->items); $this->recordPayment->execute($order); event(new OrderPlaced($order)); return $order; }); } }
Actions Don't Manage Transactions:
class CreateOrderAction { // No DB::transaction here - let UseCase handle it public function execute(OrderData $data): Order { return Order::create([ 'customer_id' => $data->customerId, 'total' => $data->total, ]); } }
Why:
- UseCase sees the full workflow atomicity requirements
- Actions stay focused and composable
- Easier to test Actions without transaction overhead
Data Flow with DTOs
Prefer DTOs over arrays for type safety:
// ✅ Type-safe with DTO readonly class OrderData { public function __construct( public int $customerId, public array $items, public string $shippingAddress, public ?string $notes = null, ) {} public static function from(array $data): self { return new self( customerId: $data['customer_id'], items: $data['items'], shippingAddress: $data['shipping_address'], notes: $data['notes'] ?? null, ); } } // Usage in Controller $order = $this->placeOrder->execute( OrderData::from($request->validated()) ); // ❌ Weak typing with arrays $order = $this->placeOrder->execute($request->validated());
Event-Driven Side Effects
Emit events instead of directly calling side effects:
class PlaceOrderUseCase { public function execute(OrderData $data): Order { $order = DB::transaction(function () use ($data) { $order = $this->createOrder->execute($data); $this->updateInventory->execute($data->items); return $order; }); // Let listeners handle side effects event(new OrderPlaced($order)); return $order; } } // Listener handles email asynchronously class SendOrderConfirmation { public function handle(OrderPlaced $event): void { $this->sendEmail->execute($event->order); } }
Benefits:
- Decouples core workflow from side effects
- Side effects can be async/queued
- Easy to add new side effects without modifying UseCase
- Better testability
Service Isolation
Services should be autonomous with clear boundaries:
✅ Good: Event-based communication
// Admin service emits event when order status changes event(new OrderStatusUpdated($order)); // Client service listens to update customer-facing status class NotifyCustomerOnStatusChange { public function handle(OrderStatusUpdated $event): void { // Client-facing notification logic } }
❌ Bad: Direct coupling
// Don't call other services directly app(ClientNotificationService::class)->notifyCustomer($order);
Cross-Service Communication:
- Events: Preferred for async workflows
- Shared Domain: Both Admin and Client services consume the same Domain models
- API contracts: For synchronous needs (via HTTP or internal interfaces)
Testing Strategy
The architecture enables comprehensive testing:
Unit Tests: Actions & Operations
test('updates product stock', function () { $product = Product::factory()->create(['stock' => 10]); $action = new UpdateStockAction(); $result = $action->execute($product, -3); expect($result->stock)->toBe(7); });
Integration Tests: UseCases
test('places order successfully', function () { $useCase = app(PlaceOrderUseCase::class); $data = OrderData::from([...]); $order = $useCase->execute($data); expect($order)->toBeInstanceOf(Order::class) ->and($order->status)->toBe(OrderStatus::PENDING); });
Feature Tests: Controllers
test('creates order via API', function () { $response = $this->postJson('/api/orders', [...]); $response->assertCreated() ->assertJsonStructure(['id', 'total', 'status']); });
Commands Reference
Service Layer Commands
| Command | Arguments | Options | Description |
|---|---|---|---|
make:service |
{name} |
- | Create a new service |
make:controller |
{name} {module} {service} |
--resource, -r |
Create a controller |
make:request |
{name} {module} {service} |
- | Create a form request |
make:use-case |
{name} {module} {service} |
- | Create a use case |
make:operation |
{name} {module} {service} |
- | Create an operation |
Domain Layer Commands
| Command | Arguments | Options | Description |
|---|---|---|---|
make:model |
{name} {domain} |
- | Create a domain model (Eloquent) |
make:action |
{name} {domain} |
- | Create a domain action |
make:dto |
{name} {domain} |
- | Create a DTO (Data Transfer Object) |
make:policy |
{name} {domain} |
- | Create a domain policy |
make:event |
{name} {domain} |
- | Create a domain event |
make:enum |
{name} {domain} |
- | Create a domain enum |
make:exception |
{name} {domain} |
- | Create a domain exception |
make:query |
{name} {domain} |
- | Create a domain query |
Best Practices
1. Keep Controllers Thin
class OrderController extends Controller { public function __construct( private PlaceOrderUseCase $placeOrder ) {} public function store(PlaceOrderRequest $request): JsonResponse { $order = $this->placeOrder->execute( OrderData::from($request->validated()) ); return response()->json($order, 201); } }
2. UseCases Orchestrate Business Workflows
class PlaceOrderUseCase { public function __construct( private CreateOrderAction $createOrder, private UpdateStockAction $updateStock, ) {} public function execute(OrderData $data): Order { return DB::transaction(function () use ($data) { $order = $this->createOrder->execute($data); foreach ($data->items as $item) { $this->updateStock->execute($item['product_id'], -$item['quantity']); } event(new OrderPlaced($order)); return $order; }); } }
3. Requests Validate
class PlaceOrderRequest extends FormRequest { public function authorize(): bool { return true; } public function rules(): array { return [ 'customer_id' => 'required|exists:customers,id', 'items' => 'required|array', 'items.*.product_id' => 'required|exists:products,id', 'items.*.quantity' => 'required|integer|min:1', ]; } }
4. One Module = One Feature
Split modules by feature within each service:
- Admin Service:
Modules/Orders— Order management (CRUD, status changes)Modules/Products— Product management (create, update, inventory)Modules/Users— User management (list, ban, roles)
- Client Service:
Modules/Orders— Place orders, view order historyModules/Cart— Cart managementModules/Account— Profile, addresses
Guiding AI Assistants
To ensure AI assistants (GitHub Copilot, Cursor, etc.) follow these architectural patterns:
Option 1: Project-Level Instructions (Recommended)
Create .github/copilot-instructions.md or .cursorrules in your Laravel project:
# Project Architecture: Pulsar + Clean Architecture This project uses Pulsar for modular, domain-driven architecture. Follow these rules: ## File Structure - Controllers: `app/Services/{Service}/Modules/{Module}/Controllers/` - UseCases: `app/Services/{Service}/Modules/{Module}/UseCases/` - Actions: `app/Domain/{Domain}/Actions/` - Models: `app/Domain/{Domain}/Models/` ## Service Model Services are delivery boundaries scoped to a consumer audience (Admin, Client), not business capabilities. The Domain layer holds all shared business logic. ## Code Rules 1. Controllers only handle HTTP - extract validated data, call UseCase, return response 2. UseCases orchestrate workflows - coordinate Actions/Operations, own transactions 3. Actions are atomic - one operation, emit events, return domain objects 4. Requests validate structure only - no business logic 5. Use constructor DI for dependencies, execute() parameters for data 6. Prefer DTOs over arrays for type safety 7. Domain layer has zero dependencies on Service layer ## Examples - See: https://github.com/faran/pulsar#architecture-best-practices
Option 2: Inline Code Comments
Add architectural hints in base classes:
<?php namespace App\Services\Client\Modules\Orders\UseCases; /** * UseCase: Orchestrates business workflow * * Rules: * - Constructor: Inject dependencies (Actions, Operations, Services) * - execute(): Accept DTOs or validated data * - Own transaction boundaries with DB::transaction() * - Emit events for side effects * - Never depend on Request objects */ class PlaceOrderUseCase { public function __construct( private CreateOrderAction $createOrder, private UpdateInventoryAction $updateInventory, ) {} public function execute(OrderData $data): Order { // Implementation } }
Option 3: Reference Documentation
In your project's README or docs/architecture.md, link directly to Pulsar patterns:
# Our Architecture We follow Pulsar's clean architecture patterns: - [Architecture Overview](https://github.com/faran/pulsar#architecture) - [Best Practices](https://github.com/faran/pulsar#architecture-best-practices) - [Layer Responsibilities](https://github.com/faran/pulsar#layer-responsibilities)
Recommended Approach
Combine all three:
.github/copilot-instructions.mdfor AI context- Base class docblocks for inline guidance
- Project docs linking to Pulsar README for team reference
This ensures both AI assistants and human developers follow consistent patterns.
Contributing
Contributions are welcome! Please follow the existing code style and commit conventions.
License
MIT License - see LICENSE file for details.
Credits
Built with ❤️ by Faran Ali
Inspired by:
- Lucid Architecture
- Clean Architecture & Vertical Slice principles
- Domain-Driven Design concepts