DTO Pattern
The DTO Pattern, or Data Transfer Object Pattern, is a software design pattern used to transfer structured data between different parts of an application. A DTO is a simple object that carries data without exposing internal models, database entities, request objects, or raw arrays everywhere in the codebase.
DTOs are commonly used in object-oriented applications, APIs, Laravel projects, Symfony applications, clean architecture, service layers, command handlers, external integrations, and data transformation workflows. They help developers make data flow clearer, safer, and easier to maintain.
Introduction
In previous articles of this OOP and Design Patterns series, we discussed patterns such as MVC Pattern, Service Layer Pattern, Repository Pattern, Dependency Injection, Command Pattern, Observer Pattern, Strategy Pattern, Adapter Pattern, and Facade Pattern.
The DTO Pattern is especially useful when data moves between these layers. For example, a controller may receive request data, pass it to a service, and the service may pass structured data to a repository, command, event, or external API adapter.
Without DTOs, developers often pass raw arrays across the application. This may work at first, but as the project grows, arrays become harder to understand, harder to validate, and easier to misuse. DTOs solve this by giving data a clear structure.
What Is a DTO?
A DTO, or Data Transfer Object, is an object created mainly to carry data from one layer to another. It usually contains properties and sometimes simple helper methods, but it should not contain complex business logic.
In simple terms, a DTO is a structured container for data.
For example, instead of passing a user registration array with keys such as name, email, password, and phone, a developer can create a RegisterUserDto. This object clearly defines which data is required for user registration.
Why the DTO Pattern Is Important
The DTO Pattern is important because it makes data transfer explicit. When a method receives an array, it is not always clear which keys are expected. The array may be missing values, contain extra values, or use incorrect key names.
With a DTO, the expected data is defined in one class. This improves readability and reduces mistakes.
DTOs also help protect internal models. Instead of passing database models or request objects everywhere, the application can pass clean data objects that contain only the values needed for a specific operation.
Problem Without DTOs
Imagine a user registration service that receives a raw array:
class UserRegistrationService
{
public function register(array $data): User
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => password_hash($data['password'], PASSWORD_BCRYPT),
]);
}
}This code works, but the service does not clearly communicate what data it expects. Does the array need a phone number? Does it include a role? Is email already validated? What happens if the password key is missing?
Raw arrays are flexible, but this flexibility can create hidden bugs. DTOs provide a clearer contract.
Solution with DTO Pattern
A DTO can make the expected data explicit:
class RegisterUserDto
{
public function __construct(
public string $name,
public string $email,
public string $password
) {
}
}The service can now receive the DTO instead of a raw array:
class UserRegistrationService
{
public function register(RegisterUserDto $dto): User
{
return User::create([
'name' => $dto->name,
'email' => $dto->email,
'password' => password_hash($dto->password, PASSWORD_BCRYPT),
]);
}
}This code is clearer. The method signature tells developers exactly what kind of data is required.
Main Purpose of DTOs
The main purpose of DTOs is to move data safely and clearly between layers. DTOs are not designed to replace business entities, models, or services. They are designed to carry data in a predictable structure.
DTOs can be used to transfer data between:
Controllers and services.
Services and repositories.
Commands and command handlers.
Events and listeners.
Application layers and external APIs.
Forms and application use cases.
Domain logic and presentation layers.
This makes the data flow easier to understand and easier to maintain.
Basic DTO Example in PHP
The following example shows a simple DTO for creating a product:
class CreateProductDto
{
public function __construct(
public string $name,
public string $description,
public float $price,
public int $categoryId,
public bool $isActive = true
) {
}
}This DTO defines all data needed to create a product. Instead of passing an unclear array, the application can pass a strongly structured object.
Using a DTO in a Service
class ProductService
{
public function create(CreateProductDto $dto): Product
{
return Product::create([
'name' => $dto->name,
'description' => $dto->description,
'price' => $dto->price,
'category_id' => $dto->categoryId,
'is_active' => $dto->isActive,
]);
}
}The service now depends on a clear data object. This improves readability and reduces the chance of using wrong array keys.
DTOs and Type Safety
DTOs improve type safety because properties can have explicit types. For example, price can be defined as float, categoryId as int, and isActive as bool.
With raw arrays, wrong values can be passed easily. A string could be passed where an integer is expected, or a required key may be missing.
Typed DTOs help catch problems earlier and make code easier to understand in IDEs and static analysis tools.
DTOs in PHP 8+
PHP 8 introduced constructor property promotion, which makes DTOs easier to write. Instead of defining properties and assigning them manually, developers can define them directly in the constructor.
class UpdateProfileDto
{
public function __construct(
public string $name,
public ?string $phone,
public ?string $city,
public ?string $country
) {
}
}This syntax is clean and practical for DTO classes.
Readonly DTOs in PHP
In modern PHP, DTOs can be made readonly to prevent accidental changes after creation.
readonly class CreateOrderDto
{
public function __construct(
public int $userId,
public array $items,
public string $paymentMethod,
public ?string $couponCode = null
) {
}
}A readonly DTO is useful when the data should remain stable after it is created. This can prevent unexpected modifications during the application flow.
Creating DTOs from Arrays
Many applications receive data as arrays from HTTP requests, forms, APIs, or JSON payloads. A DTO can provide a static factory method to create itself from an array.
class RegisterUserDto
{
public function __construct(
public string $name,
public string $email,
public string $password
) {
}
public static function fromArray(array $data): self
{
return new self(
name: $data['name'],
email: $data['email'],
password: $data['password']
);
}
}This keeps array mapping in one place and makes service code cleaner.
Using DTO from Array
$dto = RegisterUserDto::fromArray([
'name' => 'Adnan Mehrat',
'email' => 'adnan@example.com',
'password' => 'secret',
]);
$user = $registrationService->register($dto);The service receives a structured DTO instead of a raw array.
DTOs in Laravel
Laravel developers often pass validated request data as arrays to services. This is common and acceptable in small projects. However, DTOs can make the code clearer in larger applications.
A Laravel controller can create a DTO from a Form Request:
class RegisterController extends Controller
{
public function store(
RegisterUserRequest $request,
UserRegistrationService $service
) {
$dto = RegisterUserDto::fromArray($request->validated());
$user = $service->register($dto);
return response()->json($user);
}
}The Form Request handles validation, and the DTO carries the validated data into the service layer.
Laravel Form Request with DTO
A useful approach is to add a method inside the Form Request that returns a DTO:
class RegisterUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string'],
'email' => ['required', 'email'],
'password' => ['required', 'min:8'],
];
}
public function toDto(): RegisterUserDto
{
return RegisterUserDto::fromArray($this->validated());
}
}The controller becomes cleaner:
$user = $service->register($request->toDto());This keeps validation and DTO creation close to the request layer while keeping the service independent from HTTP details.
DTOs in Symfony
Symfony applications can use DTOs with forms, controllers, request payloads, Messenger commands, API Platform, and service layers.
For example, a controller can create a DTO from request data and pass it to a service:
class RegisterController
{
public function __invoke(Request $request, UserRegistrationService $service): Response
{
$dto = new RegisterUserDto(
name: $request->request->get('name'),
email: $request->request->get('email'),
password: $request->request->get('password')
);
$user = $service->register($dto);
return new JsonResponse(['id' => $user->getId()]);
}
}DTOs are also useful in Symfony Messenger when commands carry data to handlers.
DTO Pattern and APIs
DTOs are very useful in API development. APIs often receive request data and return response data. DTOs can structure both input and output.
For input, a DTO can represent the data required to create or update a resource. For output, a DTO can represent the response format that should be returned to the client.
This prevents the application from exposing internal database models directly through the API.
Response DTO Example
A response DTO can define what data should be returned to the client:
class UserResponseDto
{
public function __construct(
public int $id,
public string $name,
public string $email
) {
}
public static function fromUser(User $user): self
{
return new self(
id: $user->id,
name: $user->name,
email: $user->email
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}The API can return only the fields defined in the DTO, instead of exposing the full User model.
DTO Pattern and External APIs
DTOs are useful when integrating with external APIs. External APIs often return data in formats that do not match the internal application structure.
An adapter can convert external response data into an internal DTO. The rest of the application can then work with a stable structure.
class PaymentResultDto
{
public function __construct(
public bool $successful,
public string $transactionId,
public ?string $errorMessage = null
) {
}
}A payment adapter can return PaymentResultDto instead of returning raw provider-specific arrays.
DTO Pattern and Adapter Pattern
DTO Pattern works well with Adapter Pattern. An adapter connects incompatible systems, and a DTO can define the internal data format used by the application.
For example, Stripe and PayPal may return different response structures. Each adapter can convert the provider response into the same PaymentResultDto. The application then handles payment results consistently.
This keeps provider-specific details isolated and makes integrations easier to maintain.
DTO Pattern and Service Layer
DTOs are commonly used with the Service Layer Pattern. Controllers receive requests and create DTOs. Services receive DTOs and execute business logic.
This keeps services independent from HTTP request objects and makes them reusable from different entry points such as controllers, console commands, jobs, and tests.
For example, the same CreateProductDto can be used by an admin panel controller, an API controller, or an import command.
DTO Pattern and Command Pattern
DTOs and commands can look similar. Both may carry data. However, their intent is different.
A DTO carries data between layers. A command represents an action that should be executed.
For example, RegisterUserDto describes user registration data. RegisterUserCommand may represent the request to register a user and may be handled by a command handler.
In some architectures, command objects act like DTOs for use cases. The naming depends on the design style.
DTO vs Model
A DTO is not the same as a model. A model usually represents a domain concept or database entity. It may contain relationships, persistence logic, domain behavior, accessors, mutators, or ORM features.
A DTO is mainly used to transfer data. It should be simpler and more specific to a use case.
For example, a User model may include id, name, email, password, roles, permissions, timestamps, relationships, and methods. A RegisterUserDto may include only name, email, and password.
DTO vs Array
Arrays are flexible, but they do not clearly define structure. A method that receives an array does not show which keys are required or what types are expected.
DTOs provide a clear structure with named properties and types. They improve readability, IDE support, static analysis, and maintainability.
For small and simple operations, arrays may be enough. For larger workflows and important data transfer, DTOs are usually clearer.
DTO vs Value Object
A DTO and a Value Object are different concepts. A DTO transfers data between layers. A Value Object represents a meaningful value in the domain and often includes validation or behavior.
For example, EmailAddress can be a Value Object because it represents a valid email and may validate its format. RegisterUserDto may contain an email string or an EmailAddress object as part of user registration data.
Value Objects express domain meaning. DTOs express data transfer structure.
DTO vs Entity
An entity has identity and usually represents an object that changes over time. For example, User, Order, Product, and Invoice can be entities.
A DTO does not usually have identity or lifecycle behavior. It simply carries data for a specific operation or response.
Passing entities everywhere can expose too much internal structure. DTOs allow the application to pass only the data needed for a specific purpose.
DTO Validation
There are different opinions about whether DTOs should validate data. In many applications, validation is handled before the DTO is created, such as in a Laravel Form Request or Symfony Validator.
However, DTOs can still enforce basic type safety through constructor types. Some projects also add simple validation inside DTO factory methods.
The important point is to avoid turning DTOs into large business logic classes. Complex validation and business rules usually belong in validators, services, domain objects, or value objects.
Immutable DTOs
DTOs are often designed to be immutable. This means their values do not change after creation.
Immutable DTOs make data flow more predictable. Once a DTO is created from validated input, other parts of the application cannot accidentally modify it.
In PHP, readonly classes and readonly properties can help create immutable DTOs.
readonly class UpdateEmailDto
{
public function __construct(
public int $userId,
public string $newEmail
) {
}
}This is a clean and safe structure for data transfer.
Nested DTOs
Some data structures contain nested data. For example, an order may contain customer data and order items. DTOs can be nested to represent this structure clearly.
readonly class OrderItemDto
{
public function __construct(
public int $productId,
public int $quantity,
public float $price
) {
}
}
readonly class CreateOrderDto
{
public function __construct(
public int $userId,
public array $items,
public string $paymentMethod
) {
}
}In a stricter implementation, the items array should contain only OrderItemDto objects. This keeps the structure clear.
DTO Collections
When working with lists of DTOs, some projects create DTO collection classes. A DTO collection ensures that a list contains only specific DTO objects.
class OrderItemDtoCollection
{
private array $items = [];
public function add(OrderItemDto $item): void
{
$this->items[] = $item;
}
public function all(): array
{
return $this->items;
}
}This is useful when the collection has rules or when stronger structure is needed. For simple cases, an array of DTOs may be enough.
DTO Mapping
DTO mapping is the process of converting data from one structure into a DTO. This may include mapping from arrays, requests, models, entities, or external API responses.
Mapping can be done inside static factory methods, mapper classes, transformers, or framework-specific resources.
For small DTOs, a fromArray or fromModel method is usually enough. For complex mapping, a dedicated mapper class may be cleaner.
Mapper Class Example
class UserDtoMapper
{
public function fromUser(User $user): UserResponseDto
{
return new UserResponseDto(
id: $user->id,
name: $user->name,
email: $user->email
);
}
}A mapper can keep transformation logic separate when DTO creation becomes complex.
DTOs and API Resources
In Laravel, API Resources are often used to transform models into JSON responses. API Resources can sometimes reduce the need for response DTOs.
However, DTOs are still useful for input data, service layer data transfer, command data, external API integration, and framework-independent architecture.
The choice between DTOs and API Resources depends on the purpose. API Resources are presentation-focused, while DTOs are general data transfer objects.
Benefits of DTO Pattern
The DTO Pattern provides many benefits in object-oriented software design.
Main benefits include:
Makes data transfer explicit and structured.
Reduces reliance on raw arrays.
Improves readability and type safety.
Protects internal models from being exposed everywhere.
Improves IDE support and static analysis.
Makes service methods clearer.
Works well with APIs, services, commands, and external integrations.
Supports clean architecture and separation of concerns.
These benefits make DTOs especially useful in medium and large applications.
Drawbacks of DTO Pattern
The DTO Pattern can also have drawbacks. It adds more classes to the project, which may be unnecessary for very simple features.
Another drawback is mapping overhead. Developers must convert arrays, models, or API responses into DTOs. If overused, this can create repetitive code.
DTOs can also become confusing if they are not named clearly or if they are used for the wrong purpose. A DTO should represent a specific data transfer need, not become a generic object used everywhere.
When to Use DTO Pattern
Use the DTO Pattern when data needs a clear structure as it moves between layers.
DTO Pattern is useful when:
A service receives many input values.
Raw arrays are becoming unclear or risky.
The same data structure is passed through multiple layers.
You want to protect models from direct exposure.
You are building APIs with clear request or response formats.
You are integrating with external APIs.
You want better type safety and IDE support.
The project uses clean architecture or service layer design.
If these conditions exist, DTOs can make the design cleaner and safer.
When Not to Use DTO Pattern
Do not use DTOs for every small operation automatically. If a feature is simple and the data structure is obvious, a DTO may add unnecessary complexity.
Avoid DTOs when:
The operation has only one or two simple values.
The DTO would only duplicate a model without purpose.
The project is very small and simplicity matters more.
Mapping code becomes more complex than the problem itself.
The DTO is used as a generic object with unclear responsibility.
DTOs should improve clarity. They should not be added only to make the code look more advanced.
Common Mistakes with DTO Pattern
One common mistake is adding business logic to DTOs. A DTO should carry data, not execute complex workflows.
Another mistake is creating DTOs that are too generic. For example, UserDto may become unclear if it is used for registration, profile updates, API responses, admin views, and external integrations. Specific DTOs such as RegisterUserDto or UserResponseDto are often clearer.
A third mistake is duplicating models without reducing exposure or improving structure. A DTO should have a reason to exist.
A fourth mistake is passing raw arrays inside DTOs without clear typing when stronger structure is needed. Nested DTOs may be better for complex data.
Best Practices for DTO Pattern
To use the DTO Pattern effectively, developers should keep DTOs simple, focused, and meaningful.
Useful best practices include:
Create DTOs for specific use cases.
Use clear names such as CreateOrderDto or UserResponseDto.
Prefer typed properties.
Use readonly DTOs when data should not change.
Avoid complex business logic inside DTOs.
Use factory methods such as fromArray when helpful.
Use mapper classes for complex transformations.
Do not expose sensitive model fields in response DTOs.
Avoid creating DTOs without a clear purpose.
These practices help keep DTOs useful and maintainable.
Practical Checklist Before Creating a DTO
Before creating a DTO, developers can ask these questions:
Is raw array data becoming unclear?
Does this operation need a clear input or output structure?
Will this DTO protect internal models from exposure?
Will typed properties improve safety?
Is the DTO specific to one use case?
Will the DTO make the service method easier to understand?
Is the mapping effort worth the clarity gained?
If the answer is yes to several of these questions, a DTO may be a good design choice.
Why DTO Pattern Matters for Beginners
For beginners, DTOs may seem like extra classes at first. However, they become very useful when applications grow and data starts moving between many layers.
Learning DTOs helps beginners understand clean data flow, service layer design, API response control, external integration mapping, and type-safe application code.
DTOs also help developers move away from relying too much on raw arrays and framework-specific request objects inside business logic.
Conclusion
The DTO Pattern is a software design pattern used to transfer structured data between application layers. A DTO defines clear data fields and helps avoid unclear raw arrays, overexposed models, and tightly coupled request handling.
DTOs are useful in PHP, Laravel, Symfony, APIs, service layers, command handlers, external integrations, and clean architecture. They improve readability, type safety, maintainability, and separation of concerns.
However, DTOs should be used thoughtfully. Simple operations may not need DTOs, and DTOs should not become business logic classes. When applied correctly, the DTO Pattern is a practical tool for building clean, organized, and scalable object-oriented software.

