Photo by Azamat Esenaliev from Pexels

Clean Architecture: Entities and Models

Paul Allies

--

Introduction

Entities are domain objects that encapsulate business logic and represent the core concepts of the problem domain. Entities are not dependent on any specific implementation detail or framework and are typically defined as pure data structures. Entities are independent of the application’s use cases.

Models, on the other hand, are data structures that represent the state of the system at a particular point in time. Models are often created by mapping the data from the domain entities to a specific format that can be used to communicate with external systems.

By separating entities from models and other implementation details, Clean Architecture promotes flexibility and maintainability of the software system, making it easier to modify or extend the system over time.

Business Logic and Application Logic

In Clean Architecture, even if you have specific operations or behaviours (called use cases) in your software system, you still need entities to hold the business logic. Entities represent the core concepts and rules of your system, regardless of the specific use cases that the system will support.

Use cases coordinate the interactions between multiple entities to achieve the desired behaviour.

Entities are designed to be pure data structures that are independent of the application’s use cases and infrastructure.

Here is an example of an entity:

class Product {
constructor(
public id: number,
public name: string,
public description: string,
public price: number,
public available: boolean
) {}

canBePurchased(): boolean {
return this.available && this.price > 0;
}
}

The example is in TypeScript, but you can use the same basic concept in other programming languages too

let’s show how we can use this entity in a use case:

//define an interface for our use case
interface IPurchaseProductUseCase {
execute(product: Product, repository: ProductRepository): Promise<boolean>;
}

//create a use case that implements the interface
class PurchaseProductUseCase implements IPurchaseProductUseCase {
async execute(product: Product, repository: ProductRepository): Promise<boolean> {
if (!product.canBePurchased()) {
return false;
}

// retrieve the product from the repository
const retrievedProduct = await repository.getProductById(product.id);

if (!retrievedProduct) {
return false;
}

// perform the purchase operation
const purchaseSuccessful = await this.performPurchaseOperation(retrievedProduct);

return purchaseSuccessful;
}

private async performPurchaseOperation(product: Product): Promise<boolean> {
// perform the purchase operation
return true;
}
}

//Usage example
async function purchaseProduct() {
const useCase = new PurchaseProductUseCase();
const product = new Product(1, "iPhone", "Apple iPhone", 1000, true);
const repository = new ProductRepository();

try {
const purchaseSuccessful = await useCase.execute(product, repository);

if (purchaseSuccessful) {
console.log("Purchase successful!");
} else {
console.log("Purchase failed.");
}
} catch (error) {
console.error(`Error during purchase: ${error}`);
}
}

purchaseProduct();

Mapping

Mapping between entities and data models typically takes place in the data layer. The data source is responsible for retrieving data from the database and transforming it into a format that can be used by the repository and the use case.

//Entity
class User {
constructor(public id: number, public name: string, public email: string) {}
}

//Model
class UserDataModel {
constructor(public id: number, public username: string, public emailAddress: string) {}
}

//Mapping function that transforms the data retrieved from the database into an entity:
function mapDataModelToEntity(dataModel: UserDataModel): User {
return new User(dataModel.id, dataModel.username, dataModel.emailAddress);
}


//Usage example of a data source method
//When retrieving data from the database, we would use this mapping function to transform the database result into an entity:

async function getUserById(id: number): Promise<User> {
const result = await database.query(`SELECT * FROM users WHERE id = ${id}`);
const dataModel = result[0];

return mapDataModelToEntity(dataModel);
}

Overall, mapping between entities and data models is an important part of Clean Architecture, as it allows for the separation of concerns between the business logic and the database. By keeping the mapping logic separate from the repositories and use cases, we can make the system more flexible and maintainable over time.

Conclusion

Entities are the building blocks of the business logic.

Models are used to represent the data in a format that is suitable for display or storage. They may have different property names, data types, or formatting than the entities, depending on the specific requirements of the use case or user interface.

Mapping between entities and models is necessary to transform the data from one format to another and to keep the entities and the data models separate. This can be done in the data source implementation or in the use case or presentation layer, depending on the specific requirements of the system.

Overall, entities and models serve different purposes in the Clean Architecture design and mapping between the two is necessary to maintain the separation of concerns and make the system more flexible and maintainable over time.

--

--