Microservices with NodeJS and Seneca

Microservices with NodeJS and Seneca

Seneca is a toolkit which enables the development of modular applications based on microservices in a clear and

Seneca is a toolkit which enables the development of modular applications based on microservices in a clear and simple manner. Basically, it enables us to organize the code into actions which  are invoked by means of patterns.

It makes it possible for us to think of functionalities which collaborate with one another (business logics), to build those functionalities quickly and to escalate the services which are necessary when it is necessary. Build it Now, Scale it Later!

What is very interesting about Seneca is that it makes it possible to define and incorporate new functionalities or actions by means of plugins. For example, it is possible to expose a set of actions through an API web, user management, cms, etc.

Moreover, the communication mechanism among services is completely configurable and, in keeping with the Seneca philosophy, they can be developed or extended by means of plugins: transport already incorporated by default in HTTP channels (web service) and tcp (persistent tcp connection).

On this post we are going to focus on how to define microservices which collaborate with one another and to explore different communication mechanisms among them using the transport layer we mentioned.

A simple example

var seneca = require('seneca')()
//definiciendo la acción sum que será lanzada a través del patrón {role:'math', cmd:'sum'}
seneca.add( {role:'math', op:'sum'}, function(args,callback) {
var sum = args.left + args.right
//invocación propiamente dicha
seneca.act( {role:'math', op:'sum', left:1, right:2}, function(err,result) {
if( err ) return console.error( err )


In this case, the definition and invocation are carried out in the same node process (it is not necessary to use transport). Besides, if we command it to keep listening (decommenting the last line), the node will keep listening to requests/patterns to execute the set actions (by default HTTP in port 10101).

$ curl d ‘{“role” : “math”, “op”: “sum”, “left”: 2, “right”: 2}’http://localhost:10101/act


Now let’s look at a more complex example, with several components.


Microservices with NodeJS & Seneca

In this case we want to implement a calculator where we can enter a numerical expression and then get the result. We can access the full code published in GitHub.
The flow, as shown in the diagram, needs the calculator to delegate the parsing of the expression to the parser service, which returns an AST representing the expression. This expression will be later sent to an evaluator, which evaluates the tree nodes recursively by means of registered operations.

Each service has 4 files, one where the Seneca service is defined “<name>/service.js”; another one which implements the service itself “<name>/<name>.js”; a file which must be used by the service clients: “<name>/index.js” and another one with the specific Seneca pattern to access the service in case we want to use it again.

Service Definition

Seneca plugins are written to define services:

var name = 'calculatorService';

module.exports = function(options) {
 var seneca = this;
 var calculate = require('./calculator')(options);
 seneca.add({role: 'math', op: 'calc'}, function(args, callback) {
   calculate(args.expression, function(err, value) {
     callback(err, value)
 return { name: name };

In this case, the calculator needs the collaborators it can use to be defined and they will be passed as an argument of the function exported in this module.
To register the plugins:

var parse = getClientConfiguration('parser', argv.parser_client); 
var evaluate = getClientConfiguration('evaluator', argv.evaluator_client);
seneca.use('calculator/service', {parse: parse, evaluate: evaluate});

Calls are made inside the calculator’s code, directly to the collaborators passed as arguments, regardless of where those services are located.

module.exports = function(options) {
 var evaluate = options.evaluate;
 return function(expression, cb) {
   parse(expression, function(err, ast) {
     evaluate(ast, function(err, result) {
       cb(err, result);

To that end, a method should be defined, for example in parser/index.js, which makes the call using Seneca, if it is suitable.

module.exports = function (seneca) {
  if(((seneca || {}).constructor || {}).name  !== 'Seneca') {
     return require('./parser')
  return function(expression, cb) {
   seneca.act( {role:'math', op:'parse', expression:expression}, cb);

This way, all the services and communication channels can be configured.

Deployment scenarios

Using startService.js it is possible to launch any (or all) the previous services and define the collaborators for each of them, by means of arguments of the command line.

The following examples create a parser service in their own node process, and different communication mechanisms are used with the main service (calculator). Besides, it is understood that in the same node process of the calculator the evaluator services, sum and product are also defined, with both processes running with the same host.

It is possible, using the mechanisms that have been described, to start each service in their own process and in different hosts, or any combination desired.

Communication http

To start the parser and to make sure it listens to HTTP petitions (by default) in port 10102.

$ node startService.js parser l ‘{“port”:10102}’

To start the calculator and the evaluator, and make sure they listen to petitions in port 10101 (port by default) and they use the parser previously started.

$ node startService.js evaluator calculator parser_client=‘{“host”:”localhost”, “port”:10102}’

As HTTP services have been defined, it is possible to use any web client to make the call to the calculator.

$ curl d ‘{“role” : “math”, “op”: “calc”, “expression”: “2+2″}’http://localhost:10101/act

Communication tcp

It is also possible to make a persistent communication between the calculator and the parser simply by adding “type”:”tcp”. This will reduce the overhead of making a connection for each petition made. This type of connection is included by default and it is not necessary to download the plugin separately. In this type of communication the client will try to reconnect every time the connection is lost (in case the link’s server is reset).

Communication redis

Redis Transport is an independent plugin that has to be downloaded separately in order to use redis. In this case, redis’ publish/subscribe commands are used. Therefore, what is actually done is a broadcast of the message that is sent to all the services, listening through a given pattern, which is ideal to activate several services in the face of certain events.

$ node startService.js evaluator calculator parser_client=‘{“type”:”redis”}’

$ node startService.js parser l ‘{“type”:”redis”, “pin”:”{”role”:”math”, ”op”:”parse”}”}’

$ curl d ‘{“role” : “math”, “op”: “calc”, “expression”: “2+2″}’http://localhost:10101/act 


Incorporating a Load Balancer

Load Balance Transport is another independent plugin which allows us to add a charge balancer easily. It uses a round robin scheme by default to distribute the charge, but we can use our own algorithms if it is necessary.

$ node startService.js parser l ‘{“port”:10102}’

$ node startService.js parser l ‘{“port”:10103}’

$ node startLoadBalancer.js port 10109 workers=‘[{“port”:10102},{“port”:10103}]’

$ node startService.js evaluator calculator parser_client=‘{“port”:10109}’

$ curl d ‘{“role” : “math”, “op”: “calc”, “expression”: “2+2″}’http://localhost:10101/act



The way Seneca lets us work is very convenient; we simply develop services that collaborate with one another. When we need to escalate some components we can do it by using the transport mechanisms already mentioned.
Seneca makes it easier to trace the calls in a convenient way through their logs system. From there, we can identify gridlocks and choose the mechanism that is convenient to solve it. For example, we can move services to the same server or even in the same process and use a charge balancer, all this through the configuration.

We have only explored the plugins of the transport layer, but there are many published plugins which provide functionalities which are ready to be used.