Clean JavaScript: Using use-case interactors

Towards a cleaner software architecture

Thai Pangsakulyanont
8 min readJul 3, 2016

Sometimes, the code we write is too coupled to the framework we use. That makes our code hard to test, reuse, and reason about.

Here’s an example. We’ll use Express web framework:

Suppose we’re building a quiz game where the users can submit their answer to a question. The answer will be submitted through an HTTP POST request. All data is stored inside a MongoDB database.

  • If the answer is correct, we’ll award the point to the user.
  • If the user finished answering all questions, thus cleared the game, we want to send an email congratulating the user.
  • If the answer is not correct, then we’ll need to tell the user to try again.

Here’s one way to implement it in Express.

Here, we’ve separated the backend code into multiple services. Each service has a responsibility in its own domain. In above example, we have: AnswerService, EmailService, PointsService, and QuestionService.

Although the above code is very straightforward, and we also created service modules to separate the concerns, in my opinion, it’s still not clean.

The above code that is supposed to describe a business logic also contains too much technical details which are irrelevant to the business use case, such as:

  • where the data came from (req.user, req.params, req.body).
  • where to find the needed data (QuestionService, AnswerService, …)
  • how to award points to a user (PointsService)
  • how to send the result back to the user (res.render, EmailService)
  • the HTML templates to use (answer-correct, answer-incorrect)
  • how to determine if an answer is correct (.trim().toLowerCase())

The business use-case is hardwired to the framework and service modules, summarized using this diagram:

Your business use case directly couples to the framework.

Let’s think about unit-testing this. Because the use case is hardwired to these things, we cannot test only the unit. We need to test the unit and everything it’s hardwired to.

Then we ended up stubbing lots of things, like this:

Now there’s too much going on in this test. Instead of just testing the business logic, we’re also testing a bunch of other things as well:

  • the express router
  • the way it handles request
  • the way it sends response
  • the response code and response text

Imagine we have 10 more tests like this that tests other cases and other aspects of this business rules.

Our test suite would become a repetitive noise.

As if it cannot get any worse…

Actually, the above example is a very generous one. Some code I’ve seen — and have written — they coupled to an ORM model — something like this diagram:

There are also other tight coupling with external services such as Mailgun and Redis (which I’ll spare in this example).

In this case, it’s much harder to stub, because there are multiple ways to query the database and update the data.

We need to mock and stub the correct method. If I were to change my code from req.user.save() to UserModel.findOneAndUpdate(), then my tests are going to fail.

Stubbing things like this is not much different from writing the same code twice. At this point, it may seem easier and better to abandon unit testing and just do an integration test.

And this may seem like a good idea. It will save us from lots of stubbing. In addition, we can also verify that the system as a whole works using a single test suite. But now we need a working database and fixtures — just to test the business logic.

The business evolves…

As our business logic gets more complicated, our tests became slower and slower. For example, in the beginning, creating a question is just a matter of putting a new document into our MongoDB collection. Pretty fast.

But as our app grows, now we need to create notifications, render and send emails, update stats, put new questions into each follower’s feed, and reindex the search engine.

It keeps getting slower to a point where the tests makes us feel less productive.

Time to scale…

Our quiz application became popular and now there are way too many users than a single server can handle.

We decided that we’re going microservices.

Now, the business logic tests that depended on these MongoDB fixtures will not work anymore, because each microservice now has its own database. They may even be written by an entirely different team in a different language.

Therefore, lots of production code and tests needs to be rewritten.

We need to go back to stubbing methods, because running all these databases and microservices just to test a one-line code change is simply not practical.

Sometimes we had to use if-statements to disable some production code during the tests to speed them up. This leads to more complicated code, which makes it more bug-prone.

Technology changes…

What if instead of just HTTP requests, we need to go realtime and use WebSockets to stay ahead of the competition? What if we wanted to support both?

What if we also have to offer a REST API so that so that our quiz game can integrate with other SaaS?

For a realtime app, how about using something like Firebase or RethinkDB, which supports real-time queries out of the box, instead of MongoDB?

How fast can we move?

When business rules and implementation details are mixed, they must be rewritten each time we want to change some detail.

When we change the implementation details, it’s very easy to introduce business rule bugs if we’re not very careful — especially when our servers are burning and we need to move fast.

A cleaner architecture

Uncle Bob described the Clean Architecture as an architecture where “the business rules can be tested without the UI, database, web server, or any external element.”

If you’re new to the Clean Architecture, I really recommend watching this talk:

In JavaScript, I found this surprisingly easy to implement (which is why I wanted to write this article). The approach I use here is inspired by the Hexagonal Architecture.

First, I’ll show you a dream code that describes the use case in JavaScript. Don’t worry where the data came from and where the response will go. For me, it looks like this:

The above code describes the business rules with minimal amount of implementation details. We just assumed that somehow these variables and functions exist.

There’s a bit of necessary detail here: most of these data-accessing functions probably needs to perform some I/O, which are inherently asynchronous in Node.js world.

That’s why I need to use async and await in the above example. Some people may prefer to use generators which provides more flexibility.

The port…

If you run this function, you’ll get a lot of ReferenceErrors, because we haven’t yet specified where all these variables came from.

Instead of letting submitAnswer know where to find these variables (thus forming a tight coupling), we’re going to require the callers of submitAnswer to provide them into the use case through a minimal, well-defined interface (called a ‘port’).

What we’ve written here is a use-case interactor. It is a function that purely represents the business use case. It has no knowledge of anything other than the business rules which it’s responsible for.

All interactions with the outside world go through a port, which declares all the facilities it needs. There is a clear boundary between the business rules and implementation details.

Testing it is just a matter of sending the right value through the port into the interactor, and asserting that the right result came out:

Your business use case no longer depends on any framework. It’s totally decoupled from how data is stored, or how information is sent to the user. Our business logic tests are blazing fast.

Rather than using these frameworks as bases, they are used as tools to help us accomplish things faster.

By inverting the dependencies and segregating the interfaces, it becomes much easier to change the implementation details (e.g. provide a REST endpoint, or go realtime with WebSockets) without having to touch our use case code.

Let’s wire it to our Express app.

The delivery mechanism

Now we have a solid and well-tested use-case interactor. Let’s come back to our Express app, which is essentially one of the possible delivery mechanisms. It’s now written like this:

We sent an adapter into the interactor, which contains all the wirings needed for the use case to work inside an Express app.

As you can see, now there’s no more logic inside the Express app. No conditionals. We’re simply injecting simple functions, each one performs one thing, through the port, into the interactor.

For me, that’s good enough. We’ve separated the app into the Core and the Shell. The shell contains all the wirings for the core to work, but generally doesn’t contain any logic. All the complex business logic are now inside the core, which doesn’t depend on any framework or external services.

I would use a few integration and system-level tests against the shell to verify that all the wirings are in place.

Sometimes I don’t even have any automated test for the shell.

In one of my side-projects, I always tested the shell manually and didn’t encounter any major issue. That’s because fixing wiring problems is much, much easier than fixing logic problems.

But some people may want to unit-test the use case adapters. Although there is certainly some value in doing that, in my opinion, the benefits didn’t outweigh the costs.

Multiple ports…

Here’s the port interface for submitAnswer use case again.

export async function submitAnswer ({
answer,
question,
currentUser,
isAnswerToQuestionCorrect,
awardPoints,
recordCorrectAnswerToQuestion,
hasUserAnsweredAllQuestions,
sendCongratulationsEmailToUser,
respondWithCorrectAnswer,
respondWithIncorrectAnswer
})

It’s not easy to unit-test an adapter for a port that does so many things like this. This also means it’s hard-to-reuse.

For example, if I have to provide a REST API and WebSocket endpoints for this use-case. Many of these fields would remain be the same (e.g. isAnswerToQuestionCorrect), while the others would be different (e.g. respondWithCorrectAnswer).

In this case, I think they should be multiple ports. Here’s the refactored version:

export async function submitAnswer ({
request: {
answer,
question,
currentUser
},
data: {
isAnswerToQuestionCorrect,
awardPoints,
recordCorrectAnswerToQuestion,
hasUserAnsweredAllQuestions
},
notification: {
sendCongratulationsEmailToUser
},
response: {
respondWithCorrectAnswer,
respondWithIncorrectAnswer
}
})

Now you can build and test each port’s adapter separately:

After we’ve extracted all the ports, our Express app will look like this:

Now, it’s so minimal and clean.

To expose this use-case over WebSockets, now we only need to substitute the request and response ports, while the data and notification ports would remain the same.

Don’t forget the cognitive load!

Here’s a catch: More ports means more code. More abstraction. More wirings. More complexity. The code becomes more spread apart. It requires more mental capacity to understand.

I’d use a single port at the beginning, and then split the port only when I need to use the same interactor with many different implementation details. After all, it takes me less than 10 minutes to do it.

But having multiple ports in the beginning means that I’m placing unnecessary cognitive load on the readers of my code. I want to avoid that.

Therefore, I recommend starting simple and use one port in the beginning, and split it only when necessary.

So that’s it. A clean architecture implementation in JavaScript.

Thanks for reading!

--

--

Thai Pangsakulyanont

(@dtinth) A software engineer and frontend enthusiast. A Christian. A JavaScript musician. A Ruby and Vim lover.