jerni
a framework to build data-driven products from the ground up

Event-driven & Functional Projection

jerni is built primarily on the concepts of Event Sourcing and CQRS despite some differences from the mainstream implementations . It also embraces the functional programming techniques to simplify state-changing operations without sacrificing flexibility.

Event-Driven: You describe your business logic using a sequence of events. Those events are defined with the vocabulary of business users instead of technical terms to ensure the system is resilient to underlaying technological changes.

Functional Projection: Committed events are translated into storage layers' operations in order to persist changes. These translations are done in a functional manner. This guarantees you can always reproduce the state of your data at any point in time on any computer.

file: controllers/accounts.js
const journey = require('./journey.js');
// to make a change, commit an event
await journey.commit({
type: "ACCOUNT_CREATED",
payload: {
id: "8aud23jm",
account_name: "Alice",
currency: "USD",
}
});
events are sent through an events queue
events are sent through an events queue
file: models/accounts.js
const { Model } = require('@jerni/store-mongo');
const mapEvents = require('jerni/lib/mapEvents');
module.exports = new Model({
name: 'accounts',
transform: mapEvents({
ACCOUNT_CREATED(event) {
return {
insertOne: {
id: event.payload.id,
name: event.payload.account_name,
balance: {
[event.payload.currency]: 0
}
}
}
}
})
})

Strict Order & Exactly Once Delivery

jerni takes a few trade-offs in term of maximizing speed in order to ensure 2 important assumptions in data flow.

Strict Order: after being committed, each event will receive a globally monotonic increasing numerical ID. All processing is then strictly done following that order No Matter What™

Exactly Once Delivery: Never have to worry about missing or duplicate events. Events are not only come in order but also come once and only once. However, right when you need to, they are there for you to re-run just like the first time.

With these 2 assumptions combined, pessimistic locking is a thing in the past. Even when a server crashes, once jerni resumes (on that machine or where else), consistency is eventually restored. Race conditions will never happen in projection stage.

file: controllers/transactions.js
const journey = require('./journey.js');
// committed events are assigned with an ID
const event = await journey.commit({
type: "MONEY_SENT",
payload: {
src: "8aud23jm",
dest: "aot42lfe",
amount: 100,
src_currency: "USD",
dest_currency: "EUR",
rate: 0.88,
}
});
events are sent through an events queue
events are sent through an events queue
file: models/accounts.js
module.exports = new Model({
name: 'accounts',
transform: mapEvents({
ACCOUNT_CREATED(event) { /* ... */ },
MONEY_SENT(event) {
const {
src,
dest,
amount,
rate,
src_currency,
dest_currency,
} = event.payload;
return [
{
updateOne: {
where: { id: src },
changes: {
$inc: {
[`balance.${src_currency}`]: amount * -1
}
}
}
},
{
updateOne: {
where: { id: dest },
changes: {
$inc: {
[`balance.${dest_currency}`]: amount * rate
}
}
}
},
]
}
})
})

Developer Experience

jerni comes together with jerni-dev — a set of dev-tools to ease the development workflow locally. jerni-dev also includes test helpers to make integration tests less of a hassle.

Hot Reload Logic: Once you save a file defining a projection logic, jerni-dev will automatically reload, and your storage layers will immediately reflect the changes. If your want the change to later apply in production, simply change the version of the model, and in the next deployment, your production data will reflect.

Hot Reload Data: If you commit an event by mistake, you can browse the list of local events to modify or remove that event. Again, if a valid change is made to the list of events, your local database will immediately reflect that. It's like rewrite the history, which is forbidden in production but commonly desired in development.

Test Instance: You don't have to set up and tear down an external jerni or even jerni-dev process in order to write integration tests. An in-memory instance will seamlessly wrap your code and dispose on your request.

We want you to feel comfortable developing your product. Say good-bye to the microservices nightmare local setup and focus on the modules that you are working on.

Helpful Error Message

journey is ready!
Example app listening at http://localhost:3000
jerni-dev invalid jerni server provided, received: .

1) if you're in development mode, make sure you ran jerni-dev.
2) if you're in production mode, please set NODE_ENV=production before starting your app

Detailed Event logging

jerni-dev running in development mode

versions:
jerni: 1.0.0
jerni-dev: 1.0.0

heq-server:
original URL: undefined (not used in development mode)
dev server URL: http://localhost:6181

stores:
- mongodb://localhost:27017


jerni-dev event #1 [type=ACCOUNT_CREATED] has been committed to dev server at http://localhost:6181

Intelligent File Watcher

[ cli ] info: heq-server is listening on port 6181
[ cli ] info: writing lockfile to .jerni-dev
[ cli ] info: clean start new journey
[ cli ] info: worker ready
[jerni] info: start receiving data
[jerni] info: [ { accounts_v1: { added: 1 } } ]

[ cli ] warn: non-organic change detected!
[ cli ] info: stop watching data file
[ cli ] info: jerni subprocess stopped!
[ cli ] info: stopped watching journey source code!
[ cli ] info: heq-server subprocess stopped!
[ cli ] info: lockfile .jerni-dev removed!
[ cli ] info: heq-server is listening on port 6181
[ cli ] info: writing lockfile to .jerni-dev
[ cli ] info: clean start new journey
[ cli ] info: worker ready
[jerni] info: start receiving data
[jerni] info: [ { accounts_v1: { added: 2 } } ]

Forward Compatible

As of today, jerni uses Redis as its events store and officially provides a MongoDB store for projections. However, we are actively developing other adapters like PostgreSQL for events store and a projection for Neo4J Graph Database.

jerni is built around an open protocol based on the standard HTTP. That makes supports for languagues other than JavaScript is possible. We want to keep the API surface compact so new clients/adapters integration would be simple.