UseCase Pattern: Structuring Business Logic in Backend Development
UseCase is an architectural pattern for defining business scenarios in applications. It organizes code around specific system actions—like placing an order or user registration—rather than abstract technical operations. This approach boosts readability, simplifies testing, and scales projects effectively, especially in complex backend systems.
What is UseCase and Why Use It?
UseCase represents a complete business scenario at the application layer. It orchestrates a sequence of steps, delegating business rules to domain services and entities. For instance, instead of a low-level "write to database" operation, a UseCase describes "user places an order." This prevents business logic from sprawling across controllers and services as the project grows. Without clear scenario boundaries, code becomes hard to understand and maintain.
Key benefits of UseCase:
- Clear structure: The system is defined by explicit business capabilities, not vague abstractions like Service or Manager.
- Better readability: The architecture mirrors business logic, making it easy to answer "What can this app do?"
- Easier testing: Scenario-level integration tests verify the full user journey without breaking business processes.
Limitations and Best Practices
Avoid calling one UseCase from another to prevent hidden dependencies and turning them into reusable logic blocks. In complex scenarios, it's okay if there are no cycles and scenarios stay explicit. For reusable logic, extract it into dedicated services or domain components usable across multiple UseCases.
Where to invoke UseCase:
- Controller
- Console Command
- Job
- Event Listener
UseCase acts as the entry point to your app's business logic, mediating external interactions with the system.
Why Services Alone Fall Short
Services work fine for simple projects, but as business logic grows, issues arise. What starts as a basic "save to database" for order creation balloons into checks for stock, applying discounts, deducting balances, and sending notifications. This creates god objects—classes bloated with dependencies, methods, and side effects—making code hard to read and test.
UseCase follows a simple rule: one UseCase = one business process. Duplicate logic goes into shared services or domain components, keeping code manageable as complexity increases.
UseCase Structure in PHP and Laravel
A typical UseCase has three parts:
- Handler: Orchestrates scenario steps and conditions, without domain business logic.
- DataInput: Input data object with no logic—just typed, preferably immutable data.
- DataOutput: Result object defining a clear contract (e.g., DTO).
Example: Creating an order:
class StoreOrderDataInput
{
public function __construct(
public readonly int $userId,
public readonly array $items,
) {}
}
class StoreOrderDataOutput
{
public function __construct(
public readonly int $orderId,
public readonly string $status,
) {}
}
class StoreOrderHandler
{
public function __construct(
private OrderRepository $orders
) {}
public function handle(StoreOrderDataInput $input): StoreOrderDataOutput
{
$order = new Order(
userId: $input->userId,
);
$orderSaved = $this->orders->save($order);
return new StoreOrderDataOutput(
orderId: $orderSaved->id,
status: $orderSaved->status
);
}
}
Organizing and Testing UseCases in Projects
In Laravel, store UseCases in a structure like app/UseCases/V1 with versioning for safe contract changes. Subdirectories answer "What can the system do?"—e.g., StoreOrder, CancelOrder, RegisterUser. This mirrors business capabilities, not tech details.
Testing UseCases is straightforward with scenario-level integration tests. Example test for StoreOrderHandler:
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class StoreOrderFeatureTest extends TestCase
{
use RefreshDatabase;
public function testUserCanCreateOrder(): void
{
$user = User::factory()->create();
$payload = [
'items' => [
['id' => 10, 'qty' => 2],
['id' => 15, 'qty' => 1],
],
];
$response = $this->actingAs($user)
->postJson('/api/orders', $payload);
$response->assertStatus(200);
$response->assertJsonStructure([
'orderId',
'status'
]);
$this->assertDatabaseHas('orders', [
'user_id' => $user->id
]);
}
}
Key Takeaways
- UseCase defines business scenarios, not tech ops, for cleaner app structure.
- It solves logic sprawl in large projects, keeping code maintainable.
- One UseCase = one business process, with shared logic in services.
- Testing is simpler via integration tests covering full scenarios.
- Project structure reflects business features, speeding up onboarding.
— Editorial Team
No comments yet.