Clean Architecture: a basic example of folders organization

Tiago Albuquerque
The Dev Project
Published in
9 min readApr 5, 2022

--

Implementing a practical demo application using Typescript

First of all, it is necessary to say that the directories shown here are not a standardization, since there is no right or wrong in folders structures for projects using clean architecture.

The goal here is to show an example of a personal structure that I’ve been using and that helps me to apply clean architecture concepts for better maintenance and evolution of large systems.

Just to make it even more clear:

There is no right way for folders naming and organization in clean architecture. The directories just have to be guided by clean architecture concepts and more important is that it has to make sense to your project.

If you read about clean architecture and are trying to use it in real projects, this article may help you in your first project organization.

The motivation for studying this subject was that I had worked on some projects where business rules were spread over the system, and we could find some business rules even in views and controllers layers/classes. This situation prevented the reuse of functions and favored duplication of code.

I’ve seen also what I call “database oriented systems”, where the model reflects (and is attached to) a database model, and many business rules are expressed in SQL commands and queries, which make unit testing a tricky job.

Clean Architecture

This article assumes a basic understanding about Clean Architecture proposed by Robert C. Martin, but like many others system architecture patterns, the key goal is the separation of concerns, usually by semantic layers.

From: https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg

The main rule here is what Uncle Bob called ‘The Dependency Rule’, which says that “source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle”.

The main concern that I’ve been struggling to solve is to isolate business and application rules from frameworks and other infrastructure dependencies.

So I focused very much in ‘Entities’ and ‘Use Cases’ layer.

Project setup

To show some concepts, let’s build an API for a fake Hotel Reservation System, where there will be one endpoint to search available rooms for a given date range, and another endpoint to book a specific room.

In this project I’ll be using Typescript programming language, so to initialize the project let’s create the package.json e tsconfig.json , as well install some dependencies, using the commands below:

npm init
tsc --init
npm instal express --save
npm install uuid --save
npm install jest --save-dev
// and we can install the related TS types too...

In the tsconfig.json file I changed the output directory to: “./dist”, and created some scripts in package.json:

"scripts": {
"main": "tsc && node ./dist/index.js",
"test": "tsc && jest ./dist",
"server": "tsc && node ./dist/src/infra/http/express.js"
},

The complete version of each file is available on GitHub: package.json and tsconfig.json.

And now we are ready to code!

Let’s code!

The first directories I create is ‘src’ and ‘test’. In the ‘src’ folder, I created two more sub-folders ‘core’ and ‘infra’. The ‘core’ folder will contain classes with business logic, and ‘infra’ folder will be for specific implementations of frameworks and libraries.

In the ‘core’ folder I created the ‘entity’, ‘usecase’ and ‘repository’ folders.

I like to start with the entity classes, to model our domain. In this example, I created a class to represent a Hotel Room and another class to represent a Reservation. Of course, we could model this in many ways, but I decided that each Room will contain a list of its Reservations.

src/core/entity/Room.ts:

export default class Room {

number: number;
type: RoomType;
price: number;
reservations: Reservation[];
constructor(number: number, type: RoomType, price: number) {
this.number = number;
this.type = type;
this.price = price;
this.reservations = [];
}
}
export enum RoomType {
SINGLE = 'single',
DOUBLE = 'double',
DELUXE = 'deluxe'
}

src/core/entity/Reservation.ts

export default class Reservation {id: string | null;
room: number;
checkin: Date;
checkout: Date;
totalPrice: number;
constructor(room: number, pricePerNight: number, checkin: Date, checkout: Date) {
this.room = room;
this.checkin = checkin;
this.checkout = checkout;
this.totalPrice = this.nrOfDays * pricePerNight;
this.id = null;
}
get nrOfDays() {
const diffInTime = this.checkout.getTime() - this.checkin.getTime();
const diffInDays = Math.round(diffInTime / (1000 * 3600 * 24));
return diffInDays;
}

}

And since we have some logic in getter method ‘nrOfDays’ of Reservation, I wrote unit test (using Jest syntax) to ensure it is working:

test/core/entity/Reservation.test.ts:

describe('Reservation entity', () => {test('Should create reservation from constructor args', async function() {
const reservation = new Reservation(5, 100, new Date('2022-01-01'), new Date('2022-01-10'));
expect(reservation.nrOfDays).toBe(9);
expect(reservation.totalPrice).toBe(100 * 9);
});
});

And now we can work in the ‘use cases’ classes. In this demo application, we will create just two use cases: one for searching for available rooms and another for booking the informed room.

The Search Room use case class could be as shown below. Notice that I used the ‘execute<XXXX>’ method naming pattern, to provide search by dates and by room number operations.

Notice also that the ‘RoomRepository’ is injected in the class constructor, and later we will create this repository as an Interface, decoupling from its implementation, what is the key point here.

This class won’t compile right now, but it forces us to think about what repository methods we will need to provide.

export default class SearchRoom {private roomRepo: RoomRepository;constructor(roomRepository: RoomRepository) {
this.roomRepo = roomRepository;
}
async executebyDates(checkin: Date, checkout: Date): Promise<Room[]> {
const rooms = await this.roomRepo.getAvailableRooms(checkin, checkout);
return Promise.resolve(rooms);
}
async executeByRoomNumber(roomNumber: number): Promise<Room|undefined> {
const rooms = await this.roomRepo.findAll();
const room = rooms.find(room => room.number === roomNumber);
return room;
}
}

The Book Room use case class is shown below, again specifying the repository Interface in its constructor:

export default class BookRoom {private roomRepo: RoomRepository;constructor(roomRepository: RoomRepository) {
this.roomRepo = roomRepository;
}
async execute(room: Room, from: Date, until: Date): Promise<Reservation> {
const isAvailable = await this.roomRepo.isRoomAvailable(room.number, from, until);
if (!isAvailable) {
return Promise.reject('Room not available');
}
const reservation = new Reservation(room.number, room.price, from, until);
const persisted = await this.roomRepo.addReservation(reservation);
return persisted;
}
}

Now that we know which repository methods we will need, we can build the RoomRepository interface (stored in ‘src/core/repository’ folder):

export default interface RoomRepository {

findAll(): Promise<Room[]>;
findRoomByNumber(number: number): Promise<Room>;addReservation(reservation: Reservation) : Promise<Reservation>;getAvailableRooms(initialDate: Date, endDate: Date) : Promise<Room[]>;isRoomAvailable(room: number, initialDate: Date, endDate: Date) : Promise<Boolean>;}

To make some unit tests and not worry about databases at the beginning of the project, I like to implement a in-memory version of the repository, so I can go on with the implementation of the system and decide what database to use later.

So, in the ‘src/infra/repository’ folder, I implement the RoomRepositoryInMemory class:

export default class RoomRepositoryInMemory implements RoomRepository {

private roomsData: Room[] = [
new Room(1, RoomType.SINGLE, 100.0),
new Room(2, RoomType.SINGLE, 100.0),
new Room(3, RoomType.SINGLE, 100.0),
new Room(4, RoomType.DOUBLE, 150.0),
new Room(5, RoomType.DOUBLE, 150.0),
new Room(6, RoomType.DOUBLE, 150.0),
new Room(7, RoomType.DOUBLE, 150.0),
new Room(8, RoomType.DOUBLE, 150.0),
new Room(9, RoomType.DELUXE, 200.0),
new Room(10, RoomType.DELUXE, 200.0)
];
private reservationsData: Reservation[] = [];findAll(): Promise<Room[]> {
const rooms = [...this.roomsData];
return Promise.resolve(rooms);
}
findRoomByNumber(number: number): Promise<Room> {
const room = this.loadRoomsWithReservations().find(r => r.number === number);
return new Promise((resolve,reject) => {
if (room) resolve(room)
else reject('Room not found');
});
}

addReservation(reservation: Reservation): Promise<Reservation> {
reservation.id = uuid();
this.reservationsData.push(reservation);
return Promise.resolve(reservation);
}
getAvailableRooms(initialDate: Date, endDate: Date): Promise<Room[]> {
const rooms = this.loadRoomsWithReservations();
const availables = rooms.filter(room => this.isAvailable(room.reservations, initialDate, endDate));
return Promise.resolve(availables);
}
isRoomAvailable(room: number, initialDate: Date, endDate: Date): Promise<Boolean> {
const reservations = this.reservationsData.filter(reserv => reserv.room === room);
const isAvailable = this.isAvailable(reservations, initialDate, endDate);
return Promise.resolve(isAvailable);
}
private loadRoomsWithReservations(): Room[] {
return this.roomsData.map(room => {
const reservations = this.reservationsData.filter(r => r.room === room.number);
room.reservations = reservations;
return room;
});
}
private isAvailable(reservations: Reservation[], initialDate: Date, endDate: Date): Boolean {
const isBooked = reservations.some(r => {
return (initialDate >= r.checkin && initialDate <= r.checkout) ||
(endDate >= r.checkin && endDate <= r.checkout) ||
(r.checkin >= initialDate && r.checkin <= endDate) ||
(r.checkout >= initialDate && r.checkout <= endDate);
});
return !isBooked;
}
}

Later we can have other implementations for this repository Interface. We could have a ‘RoomRepositoryMongoDB’ and a ‘RoomRepositoryMySQL’ implementations for instance.

And now we are able to implement some unit tests to ensure the correct behavior of the use cases classes.

The SearchRoom.test.ts and BookRoom.test.ts classes are stored in ‘test/core/usecase’ folder, and there we can set the in-memory repository implemented above as the concrete repository implementation in the use case class.

describe('Search Room Use Case', () => {       let roomRepo: RoomRepository;
let searchRoom: SearchRoom;
beforeEach(() => {
roomRepo = new RoomRepositoryInMemory();
searchRoom = new SearchRoom(roomRepo);
});
// test cases ...
describe('Book Room Use Case', () => { let bookRoom: BookRoom;
let roomRepo: RoomRepository;
let room: Room;
beforeEach(() => {
roomRepo = new RoomRepositoryInMemory();
bookRoom = new BookRoom(roomRepo);
room = new Room(5, RoomType.DOUBLE, 150.0);
});
// test cases ...

The complete test classes are available here: SearchRoom.test.ts and BookRoom.test.ts.

Since our use cases seem ready and working, we can implement the http endpoints using express, and segregate the controller function in another file as well.

The ‘express.ts’ file was stored in the ‘src/infra/http’ folder, and the ‘RoomExpressController.ts’ file in the ‘src/infra/controller’ folder, since both of them are about infrasctructure code.

Since the controllers functions are within the borders of the system, they will have to know some detailed implementations, like the concrete Repository implementation.

The controller functions are listed below:

const roomRepo = new RoomRepositoryInMemory();
const searchRoomService = new SearchRoom(roomRepo);
const bookRoomService = new BookRoom(roomRepo);
export async function searchRoom(req: express.Request, res: express.Response) {
const params = req.query;
if (!params || !params.in || !params.out) {
res.status(400).json( { success: false, message: 'Invalid request' } );
return;
}
const checkin = new Date(params.in as string);
const checkout = new Date(params.out as string);
const rooms = await searchRoomService.executebyDates(checkin, checkout)
res.json( { rooms : [...rooms] } );
}
export async function bookRoom(req: express.Request, res: express.Response) {
if (!req.body || !req.body.room || !req.body.checkin || !req.body.checkout) {
res.status(400).json( { success: false, message: 'Invalid request' } );
return;
}
const room = await searchRoomService.executeByRoomNumber(req.body.room);
if (!room) {
res.status(404).json( { success: false, message: 'Room not found' } );
return;
}
const checkin = new Date(req.body.checkin);
const checkout = new Date(req.body.checkout);
try {
const reservation = await bookRoomService.execute(room, checkin, checkout);
res.json( { reservation } );
} catch(err) {
if (err instanceof Error)
res.json( { success: false, error: err.message })
}
}

The express config file content is shown below:

const app = express();app.use(express.json());app.get('/api/room/search', searchRoom);
app.post('/api/room/book', bookRoom);
app.listen(5000, () => console.log('Server running'));

The final folders structure will be like this:

I’ve seen others folders in projects as well, like ‘adapters’ to convert types from one layer to another and ‘database-config’ folder, to store specific configurations.

We can run the unit test using the script ‘npm run test’, and we use the script ‘npm run server’ to run the application and see the responses to the http endpoints using postman:

Search Room
Booking a Room

For simple projects (like this), all these layers and segregation seem over-engineering, but for large projects, it indeed helps us evolve the application with less effort.

The complete project is available on GitHub here.

Resources and further readings:

Sign up for our Free Weekly Newsletter. Fresh content from developers all over the world, fresh and direct from the field! Don’t forget to follow our publication The Dev Project.

--

--