State to decide
Introducing a common interface for handling business logic, i.e., the decide function, requires explaining three basic terms - a command, an event, and state. Commands and events are rather straightforward. We might discuss size of events, whether we should command-source or what convention to take for naming events, but the basics are rather obvious - they are just structured data that differ in intention. Commands express the request to change the state, and events are facts about what we decided happened.
But state? This is where a consultant with “it depends” enters the room.
Why
Let me shamelessly quote myself:
To express that logic in code, all you need is a
decidefunction. Adecidefunction always takes two arguments - the first one represents the intent (WHEN) of the action, the second one represents the context (GIVEN) in which it is executed.
But why do we need that context? Without it, a decide function would always return the same result. No matter how many times you call decide(TakeProductFromShelf.new(product_id: "foo-1234")), you would always get the ProductWasTakenFromShelf event, like it’s a Santa bag, not a finite inventory. Each relevant past decision evolves the state, so it can represent all the information needed to actually make a decision.
Here comes the first intuitive optimization. We should load all the information needed to make a decision, i.e., no less but also no more. Are you limiting your DB selects to columns that you actually use, or SELECT * FROM table_with_dozen_columns everywhere?

That also unveils a natural path to represent a state in any form, that would be convenient and fulfil the need. Handling a toggle? Boolean. State machine? Label. Collection? Array, set, or list, whatever your language of choice supports. Feel free to build a complex object tree if truly needed. And don’t forget about the NullObject pattern to represent the initial state, i.e., how we start, before the first action.
How
To handle any request, we need three steps:
- load the state
- decide(command, state)
- save the state
The second step already had a dedicated article, so we’re left with loading and saving. If we don’t care about surviving restarts, we can keep state in plain memory, but besides experimentation and testing this might not be sufficient.
So what does not lose data? DB tables!
class State < ApplicationRecord; end
state = State.find(params["id"])
events = decide(command, state)
events.each do |event|
case event
in FooEvent
state.name = event.name
state.status = event.status
end
end
state.save!
You might already notice that evolving the state from events seems to be a very repetitive pattern, and you will be absolutely right. That’s why the decide function often comes with the evolve function companion. The evolve function also has a standardised signature - it takes a state and an event, and returns a new version of state. Here comes the most frequent shortcut that folks take - instead of a new version, they just mutate the existing state. Is it fine? Well, it depends, I guess.
If you want to stay in the purist camp, Data#with will be your friend:
State = Data.define(:id, :name, :status)
class StateRepository
class Record < ApplicationRecord; end
def self.find(id)
record = Record.find(id)
State.new(
id: record.id,
name: record.name,
status: record.status
)
end
def self.save(state)
Record.where(id: state.id).update_all(
name: state.name,
status: state.status
)
end
private_constant :Record
end
def evolve(state, event)
case event
in FooEvent
state.with(
name: event.name,
status: event.status
)
end
end
state = StateRepository.find(params["id"])
events = decide(command, state)
state = events.reduce(state) { |state, event| evolve(state, event) } # or events.reduce(state, &method(:evolve))
StateRepository.save(state)
As you can see, you can adapt loading and saving to anything you can imagine. SQL, NoSQL, cache, memory, Actor, single table, multiple tables, anything that would fit the use case.
If you are crazy enough, who knows, maybe even event sourcing would make sense here. After some time you might even figure out that you don’t need a state at all - a loaded list of events could already be a state, and evolve is simply appending new events to that list.
Death, taxes, and changing requirements
In the “classical” approach, we make actions implicit and model with explicit state. This way of thinking has a crucial false assumption - that we can predict the future and, based on today’s requirements, we can pave reality with a concrete state representation.
But then requirements change. New ones come in. We learn (and business too!) that some ideas were wrong, others need more investment or had to be cut. And we start applying duct tape here and there, so our once beautiful, now Frankenstein model can still survive a new reality. This is where the most friction between business and dev happens - one side complains that “it takes so long” to implement something simple, the other side complains that business cannot decide how things should work. The thing is, they often don’t know, just guess or adapt to a changing business environment.
There are endless reasons to adopt event sourcing, but the main one is that you start thinking in terms of events, i.e., verbs instead of nouns. Actions, not reports. Past actions are facts — they cannot change. We can easily identify new types of actions; we can easily introduce a new version of how to represent a future action of a specific type — but we cannot change the past. What happened, that happened.
Changing how we calculate the representation (i.e., state) of past facts? One of the simplest things you can do. Business learns that past actions should mean something different tomorrow? No problem, represent a new way of understanding and voilà. You need more information? Just connect missing facts to calculation logic.
Change is no longer a problem, as we are embracing it instead.
The power, as often, lies in simplicity. Events are just small bags of data. Like with Lego bricks, you can combine them into anything you want. At any point you can modify, destroy, and rebuild what you built in a few moments if needed, but the bricks that you already have in your collection stay intact.
No need for shock therapy
The decide and evolve duo (with a little help from initial_state) has one amazing trait - you can benefit from the above without going full-on event sourcing. As my examples proved, you can still keep data as you do today in regular DB tables, while transitioning to a “thinking in events” approach. It might not eliminate all migration burden or wrong assumptions about state structure, but we have to start somewhere!
State is implicit — that’s why we didn’t provide it explicitly in tests. Even in CRUD, state is based on decisions — facts that happened — not magic. Products are not simply available on the shelf, they were delivered to the store, catalogued, inventoried. Some might be recorded as lost after stocktaking, as they were damaged or stolen. Clients are not magically available on the platform, they had to be invited or registered first, etc.
A simple switch from thinking in nouns to thinking in verbs will open a plethora of scenarios that you never thought about. That would be a win for your clients, your product, and, of course, for you.