When building a small Flutter application, you might use a single Dart class to represent data everywhere in the app. That approach can work perfectly well at first.
However, as the application grows and starts using REST APIs, Firebase, Supabase, local databases, caching, repositories, and Clean Architecture, using the same class everywhere can create unnecessary dependencies.
This is where DTOs, Models, and Entities become useful.
Although these terms are sometimes used interchangeably, they can have different responsibilities in a well-structured Flutter application.
In this guide, you’ll learn:
- What a DTO is
- What a Model is
- What an Entity is
- DTO vs Model vs Entity
- How data flows between these layers
- How to organize them in a Flutter project
- When separate classes are actually necessary
- Common mistakes developers should avoid
Quick Overview
Before going deeper, here is the basic idea:
| Type | Main Responsibility | Usually Belongs To |
|---|---|---|
| DTO | Transfer external data | Data/API layer |
| Model | Represent or transform application data | Depends on architecture |
| Entity | Represent core business data | Domain layer |
A common Clean Architecture flow looks like this:
API JSON → DTO → Entity → UI
In some projects, however, you may also see:
API JSON → DTO → Model → Entity → UI
There is no universal Flutter rule saying every application must contain all three.
The important part is understanding their responsibilities.
What Is a DTO in Flutter?
DTO stands for Data Transfer Object.
A DTO represents the structure of data entering or leaving your application.
For example, imagine an API returns:
{
"id": 101,
"full_name": "Rahul Sharma",
"email_address": "rahul@example.com",
"profile_image": "https://example.com/rahul.webp"
}
Your DTO can closely match that API response.
class UserDto {
final int id;
final String fullName;
final String emailAddress;
final String? profileImage;
const UserDto({
required this.id,
required this.fullName,
required this.emailAddress,
this.profileImage,
});
factory UserDto.fromJson(Map<String, dynamic> json) {
return UserDto(
id: json['id'] as int,
fullName: json['full_name'] as String,
emailAddress: json['email_address'] as String,
profileImage: json['profile_image'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'full_name': fullName,
'email_address': emailAddress,
'profile_image': profileImage,
};
}
}
The DTO understands the external data format.
For example, it knows that the backend calls the field:
full_name
while your Dart application uses:
fullName
That distinction becomes valuable when the backend and application use different naming conventions.
What Is the Purpose of a DTO?
The main purpose of a DTO is to create a boundary between external data and the rest of your application.
External data might come from:
- REST APIs
- GraphQL
- Firebase
- Supabase
- Local databases
- Platform channels
- Third-party SDKs
- Cached JSON
Suppose your backend changes:
{
"full_name": "Rahul Sharma"
}
to:
{
"name": "Rahul Sharma"
}
If API parsing exists throughout your application, many files may need changes.
With a DTO, you can often update the mapping in one place:
factory UserDto.fromJson(Map<String, dynamic> json) {
return UserDto(
id: json['id'],
fullName: json['name'],
emailAddress: json['email_address'],
profileImage: json['profile_image'],
);
}
Your domain layer does not necessarily need to know that the API changed.
What Is an Entity in Flutter?
An Entity represents the core business concept used by your application.
Entities are especially important when using Clean Architecture.
For example:
class UserEntity {
final int id;
final String name;
final String email;
final String? imageUrl;
const UserEntity({
required this.id,
required this.name,
required this.email,
this.imageUrl,
});
}
Notice something important.
The entity does not care that the backend fields are:
full_name
email_address
profile_image
Instead, it uses names that make sense inside the application:
name
email
imageUrl
The entity represents what a User means to your application, rather than how a particular backend represents a user.
Why Should Entities Avoid JSON Logic?
Consider this:
class UserEntity {
final int id;
final String name;
UserEntity({
required this.id,
required this.name,
});
factory UserEntity.fromJson(Map<String, dynamic> json) {
// ...
}
}
This can work technically, but in strict Clean Architecture it introduces an external data concern into the domain object.
Your domain layer should ideally not care whether its data came from:
- JSON
- Firebase
- SQLite
- Supabase
- REST API
- Cache
It simply works with business objects.
Therefore, JSON parsing generally belongs in the data layer.
Converting a DTO to an Entity
A useful pattern is to provide explicit mapping.
For example:
class UserDto {
final int id;
final String fullName;
final String emailAddress;
final String? profileImage;
const UserDto({
required this.id,
required this.fullName,
required this.emailAddress,
this.profileImage,
});
factory UserDto.fromJson(Map<String, dynamic> json) {
return UserDto(
id: json['id'],
fullName: json['full_name'],
emailAddress: json['email_address'],
profileImage: json['profile_image'],
);
}
UserEntity toEntity() {
return UserEntity(
id: id,
name: fullName,
email: emailAddress,
imageUrl: profileImage,
);
}
}
Now the flow becomes:
API
↓
JSON
↓
UserDto
↓
UserEntity
↓
Controller / State Management
↓
UI
This keeps API-specific details away from the domain layer.
What Is a Model in Flutter?
This is where developers often become confused.
Unlike DTO and Entity, the word Model does not have one universally agreed meaning in Flutter.
A model may represent:
- API data
- Local database data
- Application state
- Domain data
- A transformed data structure
- A UI-specific data object
For example, many Flutter projects call this a model:
class UserModel {
final int id;
final String name;
final String email;
const UserModel({
required this.id,
required this.name,
required this.email,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
There is nothing inherently wrong with that.
The problem begins when UserModel simultaneously becomes responsible for:
- API parsing
- Database mapping
- Business logic
- UI formatting
- State management
- Network request payloads
At that point, one class has too many responsibilities.
DTO vs Model vs Entity
The easiest way to understand the difference is to compare their responsibilities.
| Feature | DTO | Model | Entity |
|---|---|---|---|
| Represents API data | Yes | Sometimes | Usually no |
Contains fromJson() | Commonly | Often | Preferably no |
Contains toJson() | Commonly | Often | Preferably no |
| Used in domain layer | Usually no | Depends | Yes |
| Depends on API structure | Yes | Possibly | Ideally no |
| Represents business concepts | Not primarily | Sometimes | Yes |
| Changes when API changes | Often | Possibly | Ideally less often |
| Clean Architecture role | Data layer | Architecture-dependent | Domain layer |
The biggest distinction is not the class name.
It is the responsibility of the class.
Practical Flutter Example
Suppose you are building an e-commerce application.
The backend returns:
{
"product_id": 42,
"product_name": "Running Shoes",
"product_price": "2499.00",
"product_image": "https://example.com/shoes.webp",
"stock_status": 1
}
Notice that product_price is a string and stock_status is an integer.
Your application, however, would probably prefer:
double price;
bool inStock;
This is a perfect example of why separating DTOs and Entities can help.
Product DTO
class ProductDto {
final int productId;
final String productName;
final String productPrice;
final String? productImage;
final int stockStatus;
const ProductDto({
required this.productId,
required this.productName,
required this.productPrice,
this.productImage,
required this.stockStatus,
});
factory ProductDto.fromJson(Map<String, dynamic> json) {
return ProductDto(
productId: json['product_id'],
productName: json['product_name'],
productPrice: json['product_price'],
productImage: json['product_image'],
stockStatus: json['stock_status'],
);
}
}
The DTO closely represents the backend response.
Product Entity
Your application can use a cleaner representation:
class ProductEntity {
final int id;
final String name;
final double price;
final String? imageUrl;
final bool inStock;
const ProductEntity({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
required this.inStock,
});
}
Now the application does not need to know that the API sends price as a string or stock status as 0 and 1.
Mapping ProductDto to ProductEntity
extension ProductDtoMapper on ProductDto {
ProductEntity toEntity() {
return ProductEntity(
id: productId,
name: productName,
price: double.tryParse(productPrice) ?? 0,
imageUrl: productImage,
inStock: stockStatus == 1,
);
}
}
The conversion happens at the boundary between the data and domain layers.
This gives the rest of your application cleaner types.
Where Does a Model Fit?
Suppose you also store products locally using Hive, Isar, Drift, or another local persistence solution.
You might create:
ProductDto
for API communication,
ProductLocalModel
for local storage,
and:
ProductEntity
for business logic.
For example:
class ProductLocalModel {
final int id;
final String name;
final double price;
final bool inStock;
const ProductLocalModel({
required this.id,
required this.name,
required this.price,
required this.inStock,
});
}
Now each class has a clear responsibility.
Remote API
↓
ProductDto
↓
Repository
↓
ProductEntity
↓
Use Case
↓
Controller
↓
UI
Local storage might follow:
Database
↓
ProductLocalModel
↓
Repository
↓
ProductEntity
Both data sources ultimately provide the same domain object.
Recommended Flutter Project Structure
For a feature-based Flutter application, you can structure your project like this:
lib/
│
├── core/
│ ├── api/
│ │ ├── api_client.dart
│ │ └── api_endpoints.dart
│ │
│ ├── error/
│ │ ├── exceptions.dart
│ │ └── failures.dart
│ │
│ ├── utils/
│ │ ├── app_constants.dart
│ │ ├── app_helpers.dart
│ │ └── validators.dart
│ │
│ └── services/
│ └── storage_service.dart
│
└── features/
│
├── auth/
│ ├── data/
│ │ ├── dto/
│ │ │ ├── login_request_dto.dart
│ │ │ └── user_dto.dart
│ │ │
│ │ ├── models/
│ │ │ └── user_local_model.dart
│ │ │
│ │ ├── data_sources/
│ │ │ └── auth_remote_data_source.dart
│ │ │
│ │ └── repositories/
│ │ └── auth_repository_impl.dart
│ │
│ ├── domain/
│ │ ├── entities/
│ │ │ └── user_entity.dart
│ │ │
│ │ ├── repositories/
│ │ │ └── auth_repository.dart
│ │ │
│ │ └── use_cases/
│ │ └── login_use_case.dart
│ │
│ └── presentation/
│ ├── controllers/
│ │ └── login_controller.dart
│ ├── screens/
│ │ └── login_screen.dart
│ └── widgets/
│ └── login_form.dart
│
└── home/
├── data/
├── domain/
└── presentation/
This feature-first structure scales better than putting every model, controller, and screen from the entire application into global folders.
Request DTO vs Response DTO
DTOs are not limited to API responses.
They can also represent request payloads.
Consider login:
{
"email": "rahul@example.com",
"password": "password123"
}
You could create:
class LoginRequestDto {
final String email;
final String password;
const LoginRequestDto({
required this.email,
required this.password,
});
Map<String, dynamic> toJson() {
return {
'email': email,
'password': password,
};
}
}
Then your API call becomes cleaner:
final request = LoginRequestDto(
email: email,
password: password,
);
await apiClient.post(
'/login',
data: request.toJson(),
);
This avoids manually constructing maps throughout the application.
Repository Example
A repository is a useful place to hide data-source implementation details from the domain layer.
Domain repository:
abstract class UserRepository {
Future<UserEntity> getUser();
}
Notice that it returns:
UserEntity
rather than:
UserDto
The implementation can handle the DTO internally.
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
UserRepositoryImpl(this.remoteDataSource);
@override
Future<UserEntity> getUser() async {
final dto = await remoteDataSource.getUser();
return dto.toEntity();
}
}
The domain layer therefore does not need to understand the API response format.
Data Source Example
The remote data source can deal directly with DTOs.
class UserRemoteDataSource {
final ApiClient apiClient;
UserRemoteDataSource(this.apiClient);
Future<UserDto> getUser() async {
final response = await apiClient.get('/user');
return UserDto.fromJson(
response.data as Map<String, dynamic>,
);
}
}
The responsibilities are now clearly separated.
RemoteDataSource
↓
DTO
↓
Repository Implementation
↓
Entity
↓
Domain / Presentation
Using the Entity in a GetX Controller
If your project uses GetX, the controller can work with the domain entity.
class ProfileController extends GetxController {
final UserRepository repository;
ProfileController(this.repository);
final Rxn<UserEntity> user = Rxn<UserEntity>();
final RxBool isLoading = false.obs;
Future<void> loadProfile() async {
try {
isLoading.value = true;
user.value = await repository.getUser();
} finally {
isLoading.value = false;
}
}
}
Notice that the controller does not contain:
json['full_name']
or:
UserDto.fromJson(...)
Those are data-layer concerns.
Using the Entity in Flutter UI
The screen simply consumes application-friendly data.
Obx(() {
final user = controller.user.value;
if (controller.isLoading.value) {
return const CircularProgressIndicator();
}
if (user == null) {
return const Text('User not found');
}
return Column(
children: [
Text(user.name),
Text(user.email),
],
);
});
The widget does not care whether the data originated from an API, cache, Firebase, or another source.
That separation is one of the major benefits of this architecture.
What About Business Logic?
Entities can also contain domain behavior when that behavior naturally belongs to the business object.
For example:
class ProductEntity {
final double price;
final double discountPercentage;
const ProductEntity({
required this.price,
required this.discountPercentage,
});
double get discountedPrice {
return price - (price * discountPercentage / 100);
}
}
However, complex workflows involving several entities or external dependencies are usually better handled by domain services or use cases rather than putting everything inside one entity.
DTO vs Entity: Why Not Use the Same Class?
For a small application, you absolutely can.
For example:
class User {
final int id;
final String name;
User({
required this.id,
required this.name,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
);
}
}
If your application has:
- A few screens
- One simple API
- Little business logic
- No complex caching
- No multiple data sources
creating DTOs, Models, Entities, repositories, and mappers for every object may simply add boilerplate.
Architecture should solve problems rather than create unnecessary complexity.
When Should You Separate DTO and Entity?
Separation becomes more valuable when your application has:
- Multiple APIs
- Large API responses
- Firebase or Supabase alongside REST APIs
- Local caching
- Offline support
- Complex business rules
- Long-term maintenance requirements
- Multiple developers
- Backend structures that change frequently
- Unit testing requirements
- Clean Architecture
For example, imagine the backend returns:
{
"price": "2499",
"available": "Y"
}
but the application expects:
double price;
bool available;
A DTO-to-Entity mapper gives you an obvious place to perform that conversion.
When Is a Separate Model Useful?
A separate model makes sense when another layer requires its own representation.
Suppose your system contains:
API
Database
Domain
UI
You might have:
ProductResponseDto
ProductLocalModel
ProductEntity
ProductUiModel
Each exists for a specific reason.
However, do not create four classes merely because an architecture tutorial says you should.
Create another representation when it protects a boundary or solves a real problem.
UI Models
Sometimes your UI needs information that the domain entity should not contain.
Suppose your entity contains:
class OrderEntity {
final int id;
final double amount;
final DateTime createdAt;
const OrderEntity({
required this.id,
required this.amount,
required this.createdAt,
});
}
The UI might need:
Order #1042
₹2,499.00
18 Sep 2026
Instead of putting presentation formatting into the entity, a UI model can handle it.
class OrderUiModel {
final String orderNumber;
final String formattedAmount;
final String formattedDate;
const OrderUiModel({
required this.orderNumber,
required this.formattedAmount,
required this.formattedDate,
});
}
The entity remains focused on domain data, while presentation-specific formatting stays closer to the UI.
Model Naming Matters
Avoid vague classes such as:
UserModel
ProductModel
DataModel
ResponseModel
when their responsibility is not obvious.
More descriptive names are often better:
UserDto
UserEntity
UserLocalModel
UserUiModel
ProductResponseDto
ProductEntity
ProductCacheModel
LoginRequestDto
LoginResponseDto
A developer should ideally understand what a class does just by looking at its name.
Common Mistake 1: Using DTOs Directly in Widgets
For example:
Text(userDto.fullName);
This tightly connects the presentation layer to the API representation.
A better architecture is:
DTO
↓
Entity
↓
Controller
↓
Widget
Now backend changes are less likely to propagate into the UI.
Common Mistake 2: Putting API Logic Inside Entities
Avoid making your core domain object responsible for networking:
class UserEntity {
Future<void> fetchUser() async {
// API request
}
}
An entity should not generally know how to call your REST API.
That responsibility belongs in components such as:
RemoteDataSource
Repository
Common Mistake 3: Creating Too Many Classes
Overengineering is also a problem.
Consider creating:
UserDto
UserModel
UserEntity
UserData
UserResponse
UserObject
UserViewData
when all seven classes contain exactly:
id
name
email
That creates maintenance work without meaningful architectural separation.
Each representation should have a clear reason to exist.
Common Mistake 4: Passing Raw JSON Everywhere
Avoid code like:
Map<String, dynamic> user;
throughout controllers, repositories, services, and widgets.
It leads to code such as:
user['profile']['name']
appearing across your project.
Typed Dart objects provide better:
- Compile-time safety
- IDE autocomplete
- Refactoring
- Readability
- Null safety
- Testing
Parse raw external data at a defined boundary instead.
Common Mistake 5: Assuming “Model” Has One Meaning
You may see one Flutter project where:
Model = API object
and another where:
Model = database object
while another project treats its model as the domain representation.
That does not automatically mean one project is wrong.
What matters is whether the architecture defines clear responsibilities consistently.
DTO Mapping with json_serializable
For larger applications, manually writing every fromJson() and toJson() method can become repetitive.
Packages such as json_serializable can generate much of this code.
Example:
@JsonSerializable()
class UserDto {
final int id;
@JsonKey(name: 'full_name')
final String fullName;
@JsonKey(name: 'email_address')
final String email;
UserDto({
required this.id,
required this.fullName,
required this.email,
});
factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
Map<String, dynamic> toJson() =>
_$UserDtoToJson(this);
}
This is especially useful when your API contains many fields.
Using Freezed
For immutable classes, unions, equality, copyWith(), and JSON serialization, Flutter developers can also use Freezed.
A DTO might look like:
@freezed
class UserDto with _$UserDto {
const factory UserDto({
required int id,
@JsonKey(name: 'full_name')
required String fullName,
@JsonKey(name: 'email_address')
required String email,
}) = _UserDto;
factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
}
Code generation can significantly reduce repetitive mapping code, although it also introduces build-generation tooling that your team needs to maintain.
Testing Becomes Easier
Another advantage of separating DTOs and Entities is testing.
You can test API parsing independently:
test('should parse UserDto correctly', () {
final json = {
'id': 1,
'full_name': 'Rahul Sharma',
'email_address': 'rahul@example.com',
};
final dto = UserDto.fromJson(json);
expect(dto.id, 1);
expect(dto.fullName, 'Rahul Sharma');
});
Then test your mapping separately:
test('should convert UserDto to UserEntity', () {
const dto = UserDto(
id: 1,
fullName: 'Rahul Sharma',
emailAddress: 'rahul@example.com',
);
final entity = dto.toEntity();
expect(entity.name, 'Rahul Sharma');
expect(entity.email, 'rahul@example.com');
});
If the API changes later, DTO tests can catch parsing problems without necessarily affecting your domain tests.
Should Entity Extend Model?
You may occasionally see:
class UserModel extends UserEntity {
// JSON parsing
}
This reduces duplication, but it also couples the data representation to the domain structure.
A more explicit approach is often:
UserDto
↓ mapper
UserEntity
Composition and mapping make the boundary easier to see.
Still, architecture is contextual. For a small application, inheritance or even a single class may be an acceptable tradeoff.
Complete Data Flow Example
A well-separated Flutter feature might work like this:
REST API
↓
JSON Response
↓
UserDto.fromJson()
↓
UserDto
↓
Repository Implementation
↓
toEntity()
↓
UserEntity
↓
Use Case
↓
GetX / Bloc / Riverpod
↓
Flutter UI
For sending data:
Flutter UI
↓
Controller
↓
Use Case
↓
Repository
↓
Request DTO
↓
toJson()
↓
REST API
This creates a clear boundary between external data and your application’s core logic.
DTO vs Model vs Entity: Real-World Rule
Instead of memorizing definitions, ask three questions.
Does the object describe external data?
Use a DTO.
Examples:
LoginRequestDto
LoginResponseDto
ProductResponseDto
CreateOrderRequestDto
Does the object represent a core business concept?
Use an Entity.
Examples:
UserEntity
ProductEntity
OrderEntity
SubscriptionEntity
Does another layer need its own representation?
Use a clearly named Model.
Examples:
UserLocalModel
ProductCacheModel
OrderUiModel
This naming strategy is much clearer than calling everything Model.
Do You Always Need DTO, Model, and Entity?
No.
This is one of the most important points in this guide.
A simple Flutter application might only need:
UserModel
ProductModel
OrderModel
A medium-sized application might use:
UserDto
UserEntity
A large application with remote and local data sources might use:
UserResponseDto
UserLocalModel
UserEntity
UserUiModel
The number of layers should follow the complexity of your application.
More layers do not automatically mean better architecture.
Recommended Approach for Large Flutter Apps
For a production application using Clean Architecture, a practical setup is:
features/
└── product/
├── data/
│ ├── dto/
│ ├── models/
│ ├── data_sources/
│ └── repositories/
│
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── use_cases/
│
└── presentation/
├── controllers/
├── screens/
└── widgets/
Then keep shared functionality inside:
core/
├── api/
├── constants/
├── error/
├── extensions/
├── services/
└── utils/
This approach keeps each feature self-contained while still allowing reusable infrastructure.
Final Comparison
| Question | DTO | Model | Entity |
|---|---|---|---|
| What does it represent? | Transferred/external data | Layer-specific data representation | Business/domain concept |
| Where is it commonly used? | Data layer | Various layers | Domain layer |
| Should it understand JSON? | Yes, commonly | Depends | Preferably no |
| Can backend changes affect it? | Yes | Possibly | Ideally minimally |
| Can it contain business behavior? | Usually no | Depends | Yes, when appropriate |
| Should UI depend directly on it? | Usually no | Depends | Often yes through state/presentation |
| Is it mandatory? | No | No | No |
Final Thoughts
The difference between DTO, Model, and Entity in Flutter is mainly about responsibility and architectural boundaries.
A DTO represents data crossing a boundary, such as an API request or response.
An Entity represents the core business information your application actually cares about.
A Model is a broader term whose meaning depends on your architecture. It may represent local storage, cached data, API data, or presentation-specific information.
For larger Flutter projects, a clean flow such as:
API → DTO → Repository → Entity → Controller → UI
can make the codebase easier to maintain, test, and scale.
However, avoid adding DTOs, Models, Entities, and mappers simply for the sake of following a pattern. If two classes have identical responsibilities and separating them provides no real benefit, the extra abstraction may only increase boilerplate.
The goal is not to create the maximum number of layers.
The goal is to make it obvious where data comes from, where it is transformed, and which parts of your application are allowed to depend on it.




