What is the entity and how to implement the business rules?
Clean Architecture Series - Domain Layer
Continuing with the Clean Architecture Series, in this article I will explain what is an entity and how we can implement one using typescript.
For starters, Let's answer what is an entity? The entities can be describe as objects that represent the enterprise rules of the business and behavior within a specific domain.
The entity has to be unique in the system which has a set of attributes and a unique identifier that allows the system distinguish it from the other entities.
In addition, entities are independent of frameworks, databases or any external dependencies.
Having answered this, let's define the attributes and business rules of the "Policy" entity for an Insurance business system.
The attributes
Attributes of the "Policy" entity are:
name: Stores full name of the policy holder
age: Stores the age of the policy holder
smoker: Indicates if the policy holder smokes
sedentary: Indicates if the policy holder has a sedentary lifestyle.
startDate: Indicates when the policy starts.
The business rules
For the Insurance Policy Scenario let's imagine that the insurance company defines the following business rules:
Base price is $10 per month
When age is above 50 the price is doubled
Eligible to apply if age is between 16 to 70 years
When the policy holder is smoker the price is multiply by 0.4
When policy holder has sedentary life style the price is multiply by 0.7
A policy start date must be a date after today.
*if their are smoker or have a sedentary lifestyle, their become a higher risk for life insurance companies as their are more likely to make a claim.
These attributes and business rules help in maintaining the Policy data in the insurance company.
How to implement our entity?
In the first place, go to the "domain" folder and create a new folder named "policy" with a "policy.entity.ts" file. After that, we will define our entity props which represents the entity attributes:
type PolicyProps = {
id?: number;
name: string;
age: number;
smoker: boolean;
sedentary: boolean;
startDate: string;
};
In the second place, let's define our constants:
// Base price is $10 per month
const BASE_PRICE_PER_MONTH = 10;
// When age is above 50 the price is doubled
const RISK_AGE = 50;
const RISK_AGE_MULTIPLY_FACTOR = 2;
// Eligible to apply if age is between 16 to 70 years
const MINIMUM_AGE = 16;
const MAXIMUM_AGE = 70;
// When the policy holder is smoker the price is multiply by 0.4
const SMOKER_MULTIPLY_FACTOR = 0.4;
// When policy holder has sedentary life style the price is multiply by 0.7
const NO_SPORTS_MULTIPLY_FACTOR = 0.7;
Thirdly, define the PolicyEntity class:
// ...
export class PolicyEntity {
constructor(private props: PolicyProps) {
//ensuring an instance of a valid object and all setters are called at the creation time
Object.assign(this, props);
}
}
Next, to implement the business rules we are going to use the immutability and immediate validation at creation time:
Immutability: refers to the property of an object that prevents it from being modified after it is created ensures that the object's state remains unchanged throughout its lifetime, eliminating any unexpected modifications.
Immediate validation at creation time: refers to the concept of checking the validity or correctness of an object as soon as it is created which ensures that the object is in a valid state right from the moment it is created reducing the chance of passing around invalid or incorrect data.
Then, define our setters and getters to ensure have instances of valid objects
If the id is not present we assign a null value (we will delegate the id creation to the database:
private set id(id: number) {
this.props.id = id ? id : null;
}
The "age" setter helps us to ensure that the age rules apply at the creation time.
private set age(policyHolderAge: number) {
if (!policyHolderAge) {
throw new Error("The policy holder age is required");
}
// Eligible to apply if age is between 16 to 70 years
if (policyHolderAge < MINIMUM_AGE || policyHolderAge > MAXIMUM_AGE) {
throw new Error("The policy holder age has to be between 16 and 70 years");
}
this.props.age = policyHolderAge;
}
The policy "startDate" rules apply at the creation time.
private set startDate(date: string) {
const timestamp = Date.parse(date);
if (isNaN(timestamp)) {
throw Error("The policy start date is required");
}
const startDate = new Date(timestamp);
// A policy start date must be a date after today.
if (startDate <= new Date()) {
throw new Error("The policy start date must be after today");
}
this.props.startDate = date;
}
The policy price rules are calculated when the price is looked up.
private get price(): number {
// Base price is $10 per month
let price = BASE_PRICE_PER_MONTH;
// When age is above 50 the price is doubled
if (this.props.age > RISK_AGE) {
price *= RISK_AGE_MULTIPLY_FACTOR;
}
// When the policy holder is smoker the price is multiply by 0.4
if (this.isSmoker()) {
price += price * SMOKER_MULTIPLY_FACTOR;
}
// When policy holder has sedentary life style the price is multiply by 0.7
if (this.isSedentary()) {
price += price * NO_SPORTS_MULTIPLY_FACTOR;
}
return Math.round(price);
}
The rest of setter without specific rules, only ensure that the attribute is provided.
private set name(policyHolderName: string) {
if (!policyHolderName) {
throw new Error("The policy holder name is required");
}
this.props.name = policyHolderName;
}
private set sedentary(hasSedentaryLifeStyle: boolean) {
if (hasSedentaryLifeStyle === undefined) {
throw new Error("The sedentary lifestyle indicator is required");
}
this.props.sedentary = hasSedentaryLifeStyle;
}
private set smoker(regularSmoker: boolean) {
if (regularSmoker === undefined) {
throw new Error("The smoker indicator is required");
}
this.props.smoker = regularSmoker;
}
At least but not last, the rest of the getters:
private get id(): number {
return this.props.id;
}
private get age(): number {
return this.props.age;
}
private get name(): string {
return this.props.name;
}
private get startDate(): string {
return this.props.startDate;
}
private isSmoker(): boolean {
return this.props.smoker;
}
private isSedentary(): boolean {
return this.props.sedentary;
}
And we will define the unmarshall method. We might need to unmarshall our entity to a plain object. For example, to return a JSON object as a client response. Add the unmarshalled method at the bottom of the PolicyEntity:
...
unmarshalled(): Unmarshalled {
return {
id: this.id,
age: this.age,
name: this.name,
smoker: this.isSmoker(),
sedentary: this.isSedentary(),
startDate: this.startDate,
price: this.price,
};
}
Also, create the new type "Unmarshalled" below the PolicyProps type:
// For our plain object, the id becomes required and price is added.
export type Unmarshalled = Omit<PolicyProps, "id"> & {
id: number;
price: number;
};
Finally, we need a repository interface that defines the save operation which will be use in the use case. So, create a file named "policy-repository.interface.ts next to the policy entity file:
import { PolicyEntity } from "./policy.entity";
export interface IPolicyRepository {
save(policy: PolicyEntity): Promise<void>;
}
Is a simple interface which defines the save operation as a void promise with the PolicyEntity as a parameter. We will work on the implementation details later when we work on the Repository layer.
Now the Policy entity is ready to use in our clean architecture project. See you in the next article of this series.