Practical DDD in TypeScript: Repository
And how to implement the proper Anti-Corruption layer with a well-known pattern.
There are not many segments of software development that scare me, except integrations with other systems. Writing code for some business logic today looks easy with Domain-Driven Design.
However, writing code on the Infrastructure Layer to integrate some external system like 3rd party API, caching system, database, or any other type of storage, sometimes looks easy but hides potential disasters.
Disclaimer: Effective September 1st, 2023, the referral system on Medium
is no longer operational. If you appreciate this DDD series and would
like to endorse the efforts of both myself and fellow authors on Medium,
kindly demonstrate your support through claps, follows, or comments on
our blogs. Your consideration is greatly appreciated. Thank you in advance!Other articles from the DDD series:
- Practical DDD in TypeScript: Why Is It Important?
- Practical DDD in TypeScript: Value Object
- Practical DDD in TypeScript: Entity
- Practical DDD in TypeScript: Domain Service
- Practical DDD in TypeScript: Domain Event
- Practical DDD in TypeScript: Module
- Practical DDD in TypeScript: Aggregate
- Practical DDD in TypeScript: Factory
Those disasters come from the simple fact that we don’t provide a good enough layer to clean up the data that we receive before we allow it to reach the rest of our system.
We talk here about the Anti-Corruption layer. It represents a part of the code, between Bounded Context (business layer) and technical details (integrations), which make sure that information is properly mapped inside and outside, and avoids pollution for any party.
Logically, it is impossible to have such a part of the code and not to have its representation in Tactical DDD. It is, of course, the topic of this blog — the Repository pattern.
Let us dive into it.
The Anti-Corruption Layer
We should first define the exact place for the Repository pattern in our code. Although it belongs to the Domain-Driven Design, it is not clear how much it’s connected to the business logic.
As the Repository always represents a structure that keeps technical details about connection to some external world, it already does not belong to our business logic.
However, we need to access the Repository from within the domain layer. As the domain layer is the one on the bottom and does not communicate with others, we define the Repository inside of it, as an interface.
The Domain Layer
class Customer struct {
constructor(
private id: string,
private firstName: string;
private lastName: string,
//
// some fields
//
) {}
get isPerson(): boolean {
//
// some business logic
//
}
}
class Customers extends Array<Customer> {}
interface CustomerRepository interface {
getCustomer(id: string): Promise<Customer>;
searchCustomers(specification: CustomerSpecification): Promise<Customers>
createCustomer(customer: Customer): Promise<Customer>;
updateCustomer(customer: Customer): Promise<Customer>;
deleteCustomer(id: string): Promise<Customer>;
}Here CustomerRepository is a Contract that defines the method signatures we can call inside our domain. In the example above, we can find such a simple interface that defines CRUD methods.
As we defined Repository as such interface, we can use it everywhere inside the Domain Layer. It always expects and returns us results as our Entities on the Domain Layer, in this case, Customer and Customers.
This is of critical importance. The Repository communicates (arguments for methods or return values) with other parties by using the Entity from the Domain Layer.
Such Entities represent the DDD Entity pattern that holds business logic and they do not have anything with ORM Entities that map our code to the database table below (and they should be called Data Access Objects, not Entities, anyway).
The Entity Customer does not hold any information about the type of storage below. For that, we must use the Infrastructure Layer:
DAO on the Infrastructure Layer
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn
} from 'typeorm';
// this is Entity decorator from TypeORM framework
// but it is not the Entity pattern from DDD!!!
@Entity({ name: 'customers' })
class CustomerDAO extends BaseEntity {
@PrimaryGeneratedColumn()
id: string;
@Column({ name: 'first_name' })
firstName: string;
@Column({ name: 'last_name' })
lastName: string;
//
// some other fields
//
get toEntity(): Customer {
// here we force type conversion and set fallback values
return new Customer(
String(this.id),
String(this.firstName ?? ''),
String(this.lastName ?? ''),
//
// some values
//
);
}
}Notice how inside the CustomerDAO class, method toEntity, we actually enforce type conversion, to make sure we pass the right values to the business logic.
This is very important. Although TypeScript brought some conventions in JavaScript, it does not make it be strict-type programming language. On the production, you may expect any kind of type to end up there.
For JavaScript code, this type of flexibility might not be a fatal error. Still, we need to make some calculations inside the business logic. Do we expect the same value after the addition of numbers or a string and a number?
This might sound like to conservative, but that is exactly the point. If there is a place in our code where we should be as conservative as possible, that is the place inside our Anti-Corruption layer.
Repository on the Infrastructure Layer
import { Injectable } from '@nestjs/common';
@Injectable()
class CustomerDBRepository implements CustomerRepository {
constructor(
private readonly manager: EntityManager
) {}
async getCustomer(id: string): Promise<Customer> {
const customer = await this.manager.findOne(CustomerDAO, id);
return customer.toEntity;
}
//
// other methods
//
}NestJS Module
export const CUSTOMER_REPOSITORY_TOKEN = 'customerRepository';
export const customerProvider = {
provide: CUSTOMER_REPOSITORY_TOKEN,
useFactory: () => {
return new CustomerDBRepository();
},
};
@Module({
providers: [customerProvider],
})
export class CustomerModule {}Inside the example above, you may see a fragment of CustomerRepository implementation. Internally it uses TypeORM for easier integration. There you see two different structures, Customer and CustomerDAO.
The first one is a DDD Entity, where we want to keep our business logic, some domain invariants, and rules. It does not know anything about the underlying database.
The second structure is a Data Access Object, which defines how our data is transferred from and to storage. This structure does not have any other responsibility but to map the database’s data to our Entity.
The division of those two structures is the fundamental point for using the Repository as an Anti-Corruption layer in our application. It makes sure that the technical details of the table structure do not pollute business logic.
Important consequences:
- We can use one type of identifier inside our business (like UUID) and another for the database (unsigned integer). That goes for any data we want to use for the database and business logic.
- Whenever we make changes in any of those layers, we will probably make adaptations inside both DDD Entity and DAO, and the rest of the layer we will not touch (or at least destroy).
- We can decide that we want to switch to MongoDB, Cassandra, or any other type of storage. We can switch to an external API, but still, that will not affect our domain layer.
Persistence
The second feature of the Repository is Persistence. It keeps the logic for sending our data into the storage below, to keep it permanently, update, or delete.
Here is the new direction of data transformation, where we should transfer the data from the Domain Layer to the proper format that can be stored inside the database, or sent to 3rd party API, for example.
Mapper inside the DAO
@Entity({ name: 'customers' })
class CustomerDAO extends BaseEntity {
//
// some methods and fields
//
public static createFrom(customer: Customer): CustomerDAO {
const result = new CustomerDAO();
result.if = uuid4(); // create new UUID
result.firstName = String(customer.firstName ?? '');
result.lastName = = String(customer.lastName ?? '');
//
// further transformations
//
return result;
}
public updateFrom(customer: Customer) {
this.firstName = String(customer.firstName ?? '');
this.lastName = = String(customer.lastName ?? '');
//
// further transformations
//
}
}When we need unique identifiers, the Repository or Mapper is the right place. In the example above, a new UUID was generated before creating the database record.
We can do this with numbers if we want to avoid auto-incrementing from the database engine. In any case, if we do not wish to rely on database keys, we should create them inside the Repository.
Creation inside the Repository
@Injectable()
class CustomerDBRepository implements CustomerRepository {
constructor(
private readonly manager: EntityManager
) {}
async createCustomer(customer: Customer): Promise<Customer> {
const row = CustomerDAO.createFrom(customer);
const result = await this.manager.save(row);
return result.ToEntity;
}
async updateCustomer(customer: Customer): Promise<Customer> {
const row = await this.manager.findOne(CustomerDAO, customer.id);
row.updateFrom(customer)
const result = await this.manager.save(row);
return result.ToEntity;
}
//
// other methods
//
}As you can notice, all data that comes in and out of the Repository must be processed by the Repository itself or by Mapper functions, like the static createFrom or object’s updateFrom.
I can’t stress this enough, so I will do it again: always make sure that your data is safely mapped from and to your DDD Entities! And make sure you unit-test your mapping logic, otherwise, you just migrate the issue from one place to another.
Transaction Control
Sometimes we will need to store or retrieve many different data together, combining the logic from many underlying storages or database tables. A Repository is also a perfect plate to handle such transactions.
Whenever we want to persist some data and execute many queries that work on the same extensive set of tables, we should do it inside the Repository.
Transaction Control
class CustomerDBRepository implements CustomerRepository {
//
// some other methods
//
async saveCustomer(customer: Customer): Promise<Customer> {
const queryRunner = this.manager.queryRunner;
await queryRunner.startTransaction()
const row = CustomerDAO.createFrom(customer);
let result: CustomerDAO;
try {
result = await queryRunner.manager.save(row);
//
// some code
//
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
return result.ToEntity;
}
}Here Repository is a perfect place for such code. It is good that we can also make our inserts more straightforward in the future so that we will not need transactions at all.
In that case, we do not change a Contract of the Repository, but only the code inside.
Types of Repositories
We can use Repositories for other types of storage, not just relational databases. For example MongoDB or Cassandra.
We can use a Repository for keeping our cache, and in that case, it would be Redis, for example. It can even be a REST API or configurational file.
The Redis Repository
class CustomerRedisRepository implements CustomerRepository {
constructor(
private readonly client: RedisClient
) {}
async getCustomer(id: string): Promise<Customer> {
const data = await this.client.get('user-${id}')
const row = new CustomerJSON(data);
return row.ToEntity;
}
}The Third Party API
class CustomerAPIRepository implements CustomerRepository {
constructor(
private readonly client: AxiosInstance,
private readonly baseUrl: string
) {}
async getCustomer(id: string): Promise<Customer> {
const row = await this.client.get<CustomerDTO>('${this.baseUrl}/users/${id}');
return row.ToEntity;
}
}The real benefit of having a split between our business logic and technical details is to keep the same interface for the Repository, so the Domain Layer can always use it.
But when an application grows to the point where MySQL is not a perfect solution for our distributed application, in case of migration, we do not need to worry if our business logic will be affected, as long as we keep our interfaces the same.
Repository Contracts should always deal with your business logic, but your Repository implementation must use internal structures that you can map later to Entities.
Conclusion
The Repository is the well-known pattern responsible for querying and persisting data inside underlying storage. It is the main point for Anti-Corruption inside our application.
We define it as a Contract inside the domain layer and keep the actual implementation inside the infrastructure layer. It is a place for generating application-made identifiers and for running transactions.
Useful resources:
In Plain English
Thank you for being a part of our community! Before you go:
- Be sure to clap and follow the writer! 👏
- You can find even more content at PlainEnglish.io 🚀
- Sign up for our free weekly newsletter. 🗞️
- Follow us on Twitter, LinkedIn, YouTube, and Discord.






