Think in verbs! Discovering event-driven architecture

Let’s take a deeper dive into how thinking in verbs leads to the use of event-driven architecture, and just how succinct, elegant, practical, and meaningful we can be when we use Serialized’s API platform to implement our action-focused ideas.

Jaden Baptista

3/28/2022

I love linguistics — that’s my developer shtick by this point.

Here’s a fun linguistic tidbit I learned the other day: in English, we can only describe getting engaged in nouns (people and items) and adjectives (descriptive words). We can call someone a bride or a fiancée, or we can describe them by saying that they are engaged. In Portuguese, they have words like these too — an engaged man is a noivo, and you can use the same word as an adjective. But they also have a verb — an action word — to describe the action of getting engaged: noivar. We don’t have this in English! There’s a beauty in being able to succinctly describe the action: the change in state between namorados (dating people) and noivos (engaged people). We have to jump through grammatical hoops to say this in English, but it’s things like this that make Portuguese one of the most beautiful languages there is.

Now you’re probably wondering why I’ve brought up some obscure linguistical fact and what it has to do with software. Well here’s the lesson:

It’s often far more succinct, elegant, practical, and meaningful to describe a concept in verbs instead of nouns or adjectives.

This lesson applies everywhere, even in the niche world of application architecture! So let’s take a deeper dive into how thinking in verbs leads to the use of event-driven architecture, and just how succinct, elegant, practical, and meaningful we can be when we use Serialized’s API platform to implement our action-focused ideas.

Why we gravitate towards thinking in nouns and adjectives

Have you ever heard of object-oriented programming?

It’s both loved for its simplicity and passionately hated for its overuse, but here’s a more objective description from the Wikipedia article:

Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods).

This is the paradigm we’re mostly taught in computer science schools and bootcamps, the mentality that’s forced on us by class-focused languages like Java, the C variants, and Python. But when we look at OOP through the lens of linguistics, we realize something important: objects are just nouns! And we don’t have to focus on nouns!

Where does that get us? It leads to functional architecture: code driven by actions, changes, and functions. This paradigm is commonly used in JavaScript, a language that didn’t even have classes until 2015. We like to write functions instead, and then call them in loops and whatnot.

This might also seem familiar as the paradigm behind Git, the incredibly popular version-control system. While many of us just use it to pipe our code to a repository somewhere, its real power is in how it stores the way our code changes over time! Each commit is a new event, and each of those events only stores the updates from the previous commit. That architecture has proved highly efficient and durable, making it perfect for a wide range of applications, including content management systems.

What about in data storage?

There are other ways that thinking in events can help us in data storage — after all, when you think about it, we usually store objects in our databases. We store things like users or invoices. But what if instead, we stored changes? What if we kept track of actions, if we thought in verbs?

Well, that’s the goal behind event-driven data storage, something that Serialized specializes in. Let’s see what this would look like in practice. In the documentation, we find this code sample:

curl -i https://api.serialized.io/aggregates/order/723ecfce-14e9-4889-98d5-a3d0ad54912f/events \
  --header "Content-Type: application/json" \
  --header "Serialized-Access-Key: <YOUR_ACCESS_KEY>" \
  --header "Serialized-Secret-Access-Key: <YOUR_SECRET_ACCESS_KEY>" \
  --data '
  {
     "events":[
        {
           "eventId":"127b80b5-4a05-4774-b870-1c9a2e2a27a3",
           "eventType":"OrderPlacedEvent",
           "data":{
              "customerId":"some-test-id-1",
              "orderAmount":12345
           }
        }
     ]
  }

Let’s breakdown what this is doing:

  • 723ec...12f/events: We’re creating something called an “aggregate” with the id in that URL.
  • Access: We have to make sure to identify ourselves so Serialized trusts us.
  • OrderPlacedEvent: This is the event that we’re storing in our database. When we go back to look at the data, we’ll be able to aggregate this and all of the other relevant events together into the history of a single order.
  • "data":{: This is the definition of the object we’re storing as this event. We’re connecting it to a customer and giving it the amount of the order (presumably in cents in this example, since it’s an integer).

How to start thinking in verbs

This part is actually pretty straightforward — it just takes a little bit of a readjustment. Here’s the steps:

  1. Break large tasks into small pieces of logic.
  2. Ask “what is this chunk of logic doing?”
  3. Formalize those chunks as functions and events, and not as simple objects.

Let’s practice by modelling a little game where we control our player and their inventory and store all the data realtime in Serialized. Here’s what it might looks like before we add in our event-driven data storage:

class Character {
  constructor () {
    this.inventory = [];
    this.health = 100;
    this.clothes = {
      shirt: 11,  // these are just integer indexes that
      pants: 4,   // point to some array of hats or pants
      shoes: 9,
      glasses: 0,
      belt: 1,
      gloves: 1,
      jacket: 8
    };
  }
}

class InventoryItem {
  constructor () {
    this.attackDamage = 15;
    this.protectDamage = 3;
    this.wearable = false;
  }
}

class Sword extends InventoryItem {
  constructor () {
    this.attackDamage = 50;
  }
}

class Helmet extends InventoryItem {
  constructor () {
    this.wearable = true;
    this.protectDamage = 40;
  }
}

class NPC extends Character {
  constructor () {
    this.inventory = [
      new Sword()
    ];
    this.health = Infinity;
    this.clothes.glasses = 2;
  }
}

class PlayableCharacter extends Character {
  constructor () {
    this.inventory = [
      new Helmet()
    ];
  }
}

OK, so this isn’t too crazy. At it’s root, I’ve just defined some classes in Node.JS for a Character and an InventoryItem, as well as several children classes that inherit from them for a PlayableCharacter, a NPC, a Helmet, and a Sword. But this is just object-oriented programming — to transition to the verb-driven approach, let’s turn Character and InventoryItem into Serialized aggregates. This way, our derivates like Sword can just be specialized versions of their more generic aggregates instead of separate objects themselves.

Let’s try something like this instead:

// step 1: import and initialize serialized
import {Serialized} from "@serialized/serialized-client"

const serialized = Serialized.create({
  accessKey: "<YOUR_ACCESS_KEY>",
  secretAccessKey: "<YOUR_SECRET_ACCESS_KEY>"
});

// step 2: define the state of our chararcter
type CharacterState = {
  readonly inventory?: InventoryItem[],
  // we're not going to define InventoryItem right now
  // for brevity, but this is where we would

  readonly playable?: boolean
  // this separates a PlayableCharacter from a NPC
}

// step 3: create events, in this case only one
class InventoryItemPickedUp implements DomainEvent {
  constructor(
    readonly item: InventoryItem
  ) { };
}

// step 4: create a state builder for our character
class CharacterStateBuilder {
  @EventHandler(GameCreated)
  handleInventoryItemPickedUp (
    state: CharacterState,
    event: InventoryItemPickedUp
  ): CharacterState {
    return {
      inventory: [
        ...state.inventory,
        event.item // this is where we actually add an item to the inventory
      ]
    };
  }
}

// step 5: create our character aggregate
@Aggregate('character', CharacterStateBuilder)
class Character {
  constructor(
    private readonly state: CharacterState
  ) { }

  pickUpInventoryItem(
    item: InventoryItem
  ): InventoryItemPickedUp[] {
    return [new InventoryItemPickedUp(item)];
  }
}

Would you look at that? We isolated our main chunk of logic and formalized it as an event that we can call on our character aggregate with the pickUpInventoryItem function. Now that we have that done, we can keep track of a character’s inventory without actually keeping a single object for the character. Any time we want to get the whole character and it’s inventory list, we can create that data on-demand with the aggregate.

Finishing up

So what did we learn today, class?

  1. Verbs are better than nouns, both in linguistics and in programming.
  2. Simple object-focused data storage lacks the detail and power we can get from an event-driven solution like Serialized.
  3. Implementing this architecture isn’t as tough as it sounds; the hardest paert is just adapting to the new mental model.

If you’re interested in learning more, definitely check out this helpful guide for using Serialized in TypeScript, which is what I adapted a lot of the code samples above from. You don’t have to use it in TS though — Serialized has drivers for Java as well, and since it’s a simple HTTP REST API at its core, it can be used from any language if you consult these API docs.

Oh, and another lesson: Portuguese is a fantastic language. Até logo, amigos!