Domain design
Developing a Ubiquitous Language
is one of the most important challenges of domain driven design.
Once we land on a name
to express our domain. We follow a few simple conventions that help drive consistently thoughout our service.
Domain name
⚠️ Suggestion
Domain names are generally
pluralised
.Good users, projects, roles, auth
Bad user, project, role, authentications
At a minimum in any defined domain
Enso expects an interfaces.ts
file to be defined.
This helps developers easily compose how a domain has been modelled. As your domain grows in complexity, it makes sense to progressivly adopt the requests.ts
and transforms.ts
patterns to help streamline development.
An example of a common users
domain is illustrated below.
── domain
└── users
├── UserRepository.ts
├── UserService.ts
├── UsersController.ts
├── interfaces.ts
├── requests.ts
└── transforms.ts
interfaces.ts
This file should contain all the relevant interfaces for your domain that are used when designing your Factories
, Repositories
and other Services
.
/**
* Requests
*/
export interface ICreateUserRequest {
firstName: string
lastName: string
email: string
}
/**
* Repositories
*/
export interface IUserRepository {
repo: Repository<User>
browseByUuid (uuid: string, relations?: string[]): Promise<User>
}
/**
* Repositories
*/
export interface IUserSerialised {
uuid: string
name: string
email: string
}
requests.ts
If your domain accepts requests from external services or users, it’s usually a good idea to have them validated prior to passing them on to its relevant handler.
import { IsNotEmpty } from 'class-validator'
import { ICreateUserRequest } from './interfaces'
export class RegisterUserRequest implements ICreateUserRequest {
@IsNotEmpty()
firstName: string
@IsNotEmpty()
lastName: string
@IsNotEmpty()
email: string
}
This file should contain all the validators
that are used verifying inbound data.
Learn how we implement Validation in Enso.
transforms.ts
If you are sending a response back to service makes sense to have that response explicitlly serialised so you do not mistakenly send back unwanted data.
Generally we found that prefixing the Entity we want with serialise
works well in the majority of cases.
import { User } from '../../entities'
import { IUserSerialised } from './interfaces'
export const serialiseUser = function(user: User): IUserSerialised {
return {
uuid: user.uuid,
name: `${user.firstName} ${user.lastName}`,
email: user.email
}
}
TIP
By typing your responses it makes it easier for other services to implement your domain
Next
- Add a Controller; Expose a RESTful endpoint, Recipe: HTTP Controller
- Learn how to create share types across your services