Why you should not expose all entity data in the service layer

Why you should not expose all entity data in the service layer
Photo by Annie Spratt / Unsplash

It's generally better to use a well-defined interface between the service and entity layers, such as data transfer objects (DTOs) or models, that abstract the entity data and only expose the relevant information to the service layer.

For example, instead of exposing the Budget entity directly, you could create a BudgetDTO or BudgetModel that defines the data fields you want to expose and pass instances of this DTO or model to the service layer. This way, you can keep the entity layer and the service layer separate, and make changes to the entity layer without affecting the service layer.

Here is an example of a BudgetDTO in Python:

class BudgetDTO:
    def __init__(self, name: str, amount: float, start_date: str, end_date: str):
        self.name = name
        self.amount = amount
        self.start_date = start_date
        self.end_date = end_date


    def validate(self):
        if self.amount <= 0:
            raise ValueError("Amount must be positive")
        if self.start_date >= self.end_date:
            raise ValueError("End date must be after start date")

This BudgetDTO class defines four attributes: name, amount, start_date, and end_date. The data types of these attributes are specified using type annotations, but they can also be left implicit if desired.

In the service layer, you could use instances of the BudgetDTO to represent budget data without exposing the underlying entity. For example:

def create_budget(budget_dto: BudgetDTO):
    budget_dto.validate()
    budget = Budget(**budget_dto.__dict__)
    db.session.add(budget)
    db.session.commit()
    ...

With this approach, the create_budget function only needs to know about the BudgetDTO, and not the Budget entity, allowing you to keep the service layer and the entity layer loosely coupled.

You might be thinking...

wouldn't creating those DTOs duplicate whatever I have in the SQLAlchemy mapping class?

Ahh... the answer is Yes, creating a separate model such as BudgetModel can result in duplicating some of the data and logic that is already defined in the SQLAlchemy mapping class (Budget).

However, this duplication can serve as a useful separation of concerns in your application, as the BudgetModel can be used to represent and validate the data that is being passed between the API endpoint and the database, while the Budget entity can be used to represent the data that is stored in the database.

In other words, the BudgetModel provides an abstraction of the data that is being passed between the different layers of your application, and the Budget entity provides a representation of the data that is stored in the database.

By using this separation, you can keep the concerns of data validation, processing, and storage separate, and make changes to one part of the system without affecting the others. Additionally, it makes it easier to evolve the structure of your data over time, as you can make changes to the BudgetModel or the Budget entity without affecting the other parts of the system.

But...

If you don't like the duplicate then

You could skip using the separate BudgetModel or BudgetDTO class and directly use the SQLAlchemy entity (Budget) for both data storage and data validation. You can add the validation logic as methods to the entity class:

class Budget(db.Model):
    __tablename__ = 'budget'
    name = db.Column(db.String(80), primary_key=True)
    amount = db.Column(db.Float, nullable=False)
    start_date = db.Column(db.Date, nullable=False)
    end_date = db.Column(db.Date, nullable=False)

    def validate(self):
        if self.amount <= 0:
            raise ValueError("Amount must be positive")
        if self.start_date >= self.end_date:
            raise ValueError("End date must be after start date")

In the service layer, you can use the validate method to validate the data before storing it in the database:

def create_budget(data: dict):
    budget = Budget(**data)
    budget.validate()
    db.session.add(budget)
    db.session.commit()

This approach avoids duplication of code but can result in a tighter coupling between the data validation logic and the database representation, making it more difficult to change one part of the system without affecting the other.

Ultimately, the choice between these two approaches depends on the specific requirements and constraints of your application. You may want to consider factors such as maintainability, scalability, and future evolution of the system when making your decision.


Happy coding ⌨️!

Anish Shrestha

Anish Shrestha

I'm a certified TensorFlow developer and a software engineer specializing in building ai-based solutions, web applications, and everything in between.