Write Only Business Logic: Eliminate Boilerplate
Learn how declarative configuration eliminates application services, complex controllers & routing logic. And reduce code needed for building feature even by 70%.

When did we accept as normal that 60-70% of code we write is orchestration and boilerplate? Leaving just a fraction of our codebase dedicated to actually addressing real business challenges. Multiple Controllers that do transformations and delegation, Application Services that are only there to execute method on object and then save it. Endless boilerplate that obscures the real business logic buried somewhere in the middle.
What if you could skip all that technical noise and write only the code that matters to your business?
The Hidden Cost of Traditional Architecture
Let me show you what a typical "simple" user registration looks like in most PHP applications:
<?php
// Controller layer
class UserController
{
public function __construct(
private UserApplicationService $userApplicationService
) {}
public function register(Request $request): Response
{
$command = new RegisterUserCommand(
Email::fromString($request->get('email')),
$request->get('name')
);
$this->userApplicationService->registerUser($command);
return new JsonResponse(['status' => 'success']);
}
}
// Application Service layer
class UserApplicationService
{
public function __construct(
private UserRepository $userRepository,
private EventBus $eventBus
) {}
public function registerUser(RegisterUserCommand $command): void
{
$user = User::register($command);
$this->userRepository->save($user);
$this->eventBus->publish(new UserRegisteredEvent($user->getId()));
}
}
// Domain layer
class User
{
public function __construct(
private UserId $userId,
private Email $email,
private string $name
) {
$this->recordThat(new UserRegistered($this->userId));
}
public static function register(RegisterUser $command): self
{
return new self(UserId::generate(), $command->email, $command->name);
}
}
Count the lines: 34 lines of technical orchestration for what should be a simple business operation. And this is just the beginning - add error handling, transactions, and you're looking at 50+ lines before you even touch the actual user registration method.
Every line of technical code is a line that doesn't solve your customer's problem.
The DDD Promise vs. Reality
Domain-Driven Design promises to put business logic at the center, but traditional implementations often make things worse. You end up with:
- Application Services that just delegate to domain objects
- Controllers that know too much about domain concepts
- Command/Query handlers scattered across multiple layers
- Routing logic duplicated in multiple places
- Endless transformations between layers
The business logic gets lost in a maze of technical abstractions. Developers spend more time navigating layers than solving problems. This creates narrative that Hexagonal, Layered Architecture, CQRS and DDD is bunch of buzz words which brings complexity rather than simplicity.
But if it would be so bad at the end, big minds of programming wouldn't be promoting this as a way to build cohesive maintainable software. Therefore there must be a way to approach that from different perspective, and that perspective emerges from Declarative Configuration.
Declarative Configuration: A Different Approach
What if instead of writing all that orchestration code, you could simply declare your intentions and write only the business part of the code?
Here's the same user registration using Ecotone's declarative approach:
<?php
#[Aggregate]
class User
{
use WithEvents;
public function __construct(
#[Identifier] private UserId $userId,
private Email $email,
private string $name
) {
$this->recordThat(new UserRegistered($this->userId));
}
#[CommandHandler("user.register")]
public static function register(RegisterUser $command): self
{
return new self(UserId::generate(), $command->email, $command->name);
}
}
User is our Core Business Object (Entity/Aggregate/Model whatever we call it). Ecotone will call this factory method, get instance of new User Aggregate and then store it.
Aggregates can be stored with inbuilt support Doctrine ORM entities, Laravel Eloquent model or any other object with custom repository.
That's it. No application service. No manual routing. Just pure business logic with a simple declaration that this method handles the "user.register" command.
Ecotone handles all the technical concerns - routing, persistence, events - while you focus on business rules.
Universal Controllers: Two Controllers for Your Entire Application
Here's where it gets really powerful. With routing keys, you can handle your entire application with just two controllers:
<?php
class CommandController
{
public function execute(Request $request, CommandBus $commandBus): Response
{
$routingKey = $request->headers->get('X-Routing-Key');
$commandBus->sendWithRouting(
routingKey: $routingKey,
command: $request->getContent(),
commandMediaType: "application/json"
);
return new JsonResponse(['status' => 'success']);
}
}
class QueryController
{
public function execute(Request $request, QueryBus $queryBus): Response
{
$routingKey = $request->headers->get('X-Routing-Key');
$result = $queryBus->sendWithRouting(
routingKey: $routingKey,
query: $request->getContent(),
queryMediaType: "application/json"
);
return new JsonResponse($result);
}
}
Two controllers. That's your entire web layer. The frontend sends a routing key like "user.register" or "order.place", and Ecotone automatically:
- Deserializes the JSON into the correct Command/Query object
- Routes to the appropriate handler method and aggregate
- Handles persistence, transactions, and events
- Returns the response with assigned identifier
This solution makes it extremely easy to roll out new features, as all Developers need to do is to provide an method on the Aggregate and mark it with Command Handler.
This solution works not only for REST API, but also in context of GraphQL. It take care completely of incoming command and queries making effortless to expose and connect things together.
Real-World Example: E-commerce Order Processing
Let's see how this works with a more complex business scenario - processing an order:
<?php
#[Aggregate]
class Order
{
#[CommandHandler("order.place")]
public static function place(PlaceOrder $command): self
{
// Business validation
if (empty($command->items)) {
throw new EmptyOrderException();
}
return new self($command->orderId, $command->customerId, $command->items);
}
#[CommandHandler("order.confirm")]
public function confirm(ConfirmOrder $command): void
{
if ($this->status !== OrderStatus::PENDING) {
throw new InvalidOrderStateException();
}
$this->status = OrderStatus::CONFIRMED;
}
#[QueryHandler("order.getStatus")]
public function getStatus(): OrderStatus
{
return $this->status;
}
}
Your frontend can now:
- Place an order:
POST /command
with headerX-Routing-Key: order.place
- Confirm an order:
POST /command
with headerX-Routing-Key: order.confirm
- Check status:
GET /query
with headerX-Routing-Key: order.getStatus
All routing, deserialization, and persistence happens automatically. You wrote only business logic.
The Architecture That Emerges
This declarative approach naturally creates a clean, maintainable architecture:
Domain Layer: Pure business logic in Aggregates with Command/Query handlers
Infrastructure Layer: Handled entirely by Ecotone's configuration
Application Layer: Eliminated - commands go directly to domain objects
Presentation Layer: Two universal controllers that route based on intent
When you remove technical boilerplate, what remains is pure business value.
Benefits You'll Experience Immediately
- Faster Development: No more writing application services, complex controllers, or routing logic. Focus on business rules.
- Simpler Testing: Test business logic directly without mocking infrastructure concerns.
- Better Maintainability: Changes to business rules happen in one place - the domain object.
- Clearer Intent: Routing keys make the system's capabilities explicit and discoverable.
- Reduced Complexity: Fewer layers, fewer abstractions, fewer places for bugs to hide.
- Easier AI Development: If you use AI for development, you will get better results with generated code, as context provided to AI Model will be much smaller, therefore more focused.
The Simple Architecture Promise
Write only business logic. Let declarative configuration handle the technical concerns. Use Command Handlers directly on Aggregates to eliminate application services. Route everything through two universal controllers using routing keys.
This isn't just cleaner code - it's a fundamentally different way of thinking about application architecture. Instead of building technical scaffolding around business logic, you declare business intentions and let the Ecotone provide the scaffolding and abstract away repetitive code.
The result of this? Applications that are easier to understand, faster to develop, and simpler to maintain. Applications, where every line of code serves a business purpose.
Your customers don't care about your application services. They care about the problems you solve. Focus on what matters.