This post can be read independently but is part of a series of posts about Ports and Adapters and Functional programming. If you want to read it, you can access through this link.
User Registration
We need to implement a service that will manage the user registration for a web application. The system needs to allow users to get registered and to confirm their registration and email address through a link that will be sent in an automatic email.
The emails are sent by a different service that contains this functionality. The User Service will need to publish events to the event bus in order to communicate with the rest of the system and will need to store the information about the user in its own database.
Note: we are going to simulate the database and the messaging system because it goes beyond the goal of this post.
Architecture
We are going to follow a simple Layered Architecture, from HTTP API to Database / Event Bus:

We are going to focus on the elements within the dotted box:
- HTTP Facade: offers the HTTP interface for the clients to connect and access the functionality of the service.
- Service: contains the business of the service. Executes the behaviour defined by the domain rules.
- Repository: offers the methods required for storing and retrieving the data from the database.
- Events adapter: offers the methods required for publish and subscribe to events in the event bus. In this case, the user service will only publish them.
Coding
You can find the code related to this post in GitHub.
I am going to start the development from top to bottom since it is easier to reason about the layers in Layered Architecture.
HTTP Facade
The HTTP Facade will contain the entry point to the User Service. As we said we will need two different methods: registerUser and confirmUser.
Note: since I want to discuss about the concepts I am not using REST or OData or GraphQL APIs, since they require a whole (big) post on their own.
Our initial version of the Http Facade will be as simple as:
const express = require('express');
const app = express();
app.post('registerUser', (req, res) => {
});
app.post('confirmUser', (req, res) => {
});
app.listen(3000, () => {
console.log('Listening to port 3000');
});
As you can see with express it is very easy to implement the facade. We just need to choose the endpoints of the api (both path and verbs).
Of course now the HTTP Facade is doing nothing, but we still do not have a service to call. We are going to include the User Service to see how the final picture of this layer will look like.
As shown in the diagram, the User Service will have some dependencies on the repository and the events adapter. We are going to inject those dependencies in the class constructor, so we can abstract the service from the implementation of these dependencies so we can test it better by mocking them in the tests. ç
We could have a factory taking care of injecting the dependencies to the service, or a bootstrapper configuring some of the dependencies map through a IOC library (like infusejs for example) but for simplicity we are going to initialise and inject the dependencies in the HttpFacade.
The registerUser method would look like:
app.post('registerUser', async (req, res) => {
const userService = new UserService(new UserRepository(), new EventAdapter());
const result = await userService.registerUser(userData);
res.send(result);
});
The code consists in simply :
- Initialise a new UserService passing its dependencies in the constructor.
- Retrieve the data sent to the api endpoint.
- Call the right method in the UserService.
- Return the result to the caller (we are ignoring potential errors to make the code simpler).
The confirmUser endpoint will do something similar, just calling a different method in the service. The UserService initialisation will be the same, and it could be that UserRepository or EventAdapter had dependencies, so it would be nice to have a factory or use an IOC library as mentioned before.
We are going to create the User Service next.
User Service
The user service will contain all the domain or business logic. It needs to apply the rules, validations, etc to ensure that the actions we are trying to perform are correct before persisting the result or new state in the chosen storage system and notifying the rest of the system about this operation.
module.exports = class UserService {
constructor(repository, eventAdapter) {
this.repository = repository;
this.eventAdapter = eventAdapter;
}
async registerUser(userData) {
if(!userData) {
throw new Error('The user cannot be null.');
}
validateUserName(userData.userName);
validatePassword(userData.password);
const user = {
userName: userData.userName,
password: userData.password,
state: 'PENDING'
};
const newUser = await this.repository.save(user);
this.eventAdapter.publish('user.registered', newUser);
return newUser;
}
async confirmUser(userData) {
if(!userData) {
throw new Error('The user cannot be null.');
}
let user = await this.repository.getByNameAndPassword(userData.userName, userData.password);
user.state = 'CONFIRMED';
await this.repository.save(user);
this.eventAdapter.publish('user.confirmed', user);
return user;
}
}
function validatePassword(password) {
// Validate length, strength, etc
}
function validateUserName(userName) {
// validate length, format, etc.
}
The code is simple but here is what this class is doing:
constructor:
- Stores the dependencies so they can be used whenever needed.
registerUser:
- Validates that the user data is correct.
- Sets some fields based on the domain rules: a user starts as PENDING until the email is confirmed.
- Persists the information in the database.
- If everything above is correct it will publish a new event to notify the rest of the system about the new user.
confirmUser: (see registerUser)
Although is beyond this topic, I wanted to write a short comment about events:
In this example we are using events to communicate through different services. In this specific case, we might need to sent an Email to the user that just registered so he can confirm its email address. Since one application can send emails due to different reasons or from different actions that might be implemented in different services, it makes sense to have a separated service for the Emails. It is very important to understand that an event represent something that happened. I should take special care on not sending the event before knowing that the action was performed and persisted, otherwise we could provoke a lot of inconsistencies in the system (for example sending an email to the user that was not persisted in the DB or even that had some error in a field and the validation did not success).
In our example EventAdapter and Repository are doing nothing, so we will not show the code in this post. But again you can download the full code from GitHub.
Unit Tests
We are going to test the service to ensure that the functionality does what we expect and follows the requirements. We are going to install chai and mocha for its execution ( npm install chai mocha --dev ).
In order to test the UserService in isolation we will need to mock (create predictable versions) of the dependencies. We can use tools like sinon for this task, but in this case, I am going to create very basic mocks.
We are going to test the registerUser method only as an example.
In the code bellow you will see that we need chai and chai-as-promised to create our tests.
The first test will just ensure that a registering a user sending null will throw an exception. We need to use the "rejectedWith" method because the userService returns Promises when running the registerUser method.
const chai = require('chai');
const UserService = require('./user-service');
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('user-service', () => {
describe('registerUser', () => {
it('If user data is null, registerUser should throw an exception', () => {
const userService = new UserService();
expect(userService.registerUser(null)).to.be.rejectedWith(Error);
});
it('If user data is null, registerUser should throw an exception', () => {
const userService = new UserService();
expect(userService.registerUser(null)).to.be.rejectedWith(Error);
});
})
})
A more interesting test would be a happy case. We will assume that the user is correct and the repository and the eventAdapter work properly. For that we will need to create mocks of the dependencies used by UserService.
Creating two methods we will use for testing the happy case:
const doNothing = () => {};
const returnArgument = (arg) => arg;
Testing that saving the user data returns the same data plus the new state. We do not check about the new Id generated since it is responsibility of the repository to create it.
it('If user data is correct and the user can be saved and the event published the service will return the new user', async () => {
const userData = {
userName: 'user-name',
password: 'some-password'
}
const userService = new UserService({ save: returnArgument }, { publish: doNothing });
const newUser = await userService.registerUser(userData);
expect(newUser.userName).to.equal(userData.userName);
expect(newUser.password).to.equal(userData.password);
expect(newUser.state).to.equal('PENDING');
});
We can create more tests by thinking on what could occur when using the registerUser method and deciding what should be the right reaction on each case. For example, what if the user could not be saved? what if the user could be saved but the event could not be published? etc.
Final questions
We did not add any log to our service. If we wanted to use a logger module, should we inject it through the constructor? Should we just import it form the node_modules?
What about other dependencies like
lodash or
uuid, etc. Should we inject all the utility libraries through the constructor? Should we allow modules to import other modules directly?
Sometimes deciding what should be injected and what should be just imported is not easy.
One possible criteria is injecting all modules that produce non deterministic behaviours and just import all the ones that produce deterministic responses. It would mean that all modules executing I/O operations, generating uuid or random values, etc. should be injected.
The logger for example, is we assume that it writes only to console.log, we could assume it as deterministic (almost) but if it can write to files or send events, then it would be clearly non deterministic and therefore it should be injected to all modules using it.