<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://blog.arkency.com/">
  <id>https://blog.arkency.com/</id>
  <title>Hi, we're Arkency</title>
  <updated>2026-09-27T10:00:00Z</updated>
  <link rel="alternate" href="https://blog.arkency.com/" type="text/html"/>
  <link rel="self" href="https://blog.arkency.com/atom.xml" type="application/atom+xml"/>
  <author>
    <name>Arkency</name>
    <uri>https://arkency.com</uri>
  </author>
  <entry>
    <id>tag:blog.arkency.com,2026-09-27:/6-levels-of-knowledge-management-maturity-in-organizations/</id>
    <title type="html">6 levels of knowledge management maturity in organizations</title>
    <published>2026-09-27T10:00:00Z</published>
    <updated>2026-09-27T10:00:00Z</updated>
    <author>
      <name>Piotr Jurewicz</name>
      <uri>https://blog.arkency.com/authors/piotr-jurewicz/</uri>
    </author>
    <link rel="alternate" href="https://blog.arkency.com/6-levels-of-knowledge-management-maturity-in-organizations/" type="text/html"/>
    <content type="html">&lt;h1 id="6_levels_of_knowledge_management_maturity_in_organizations"&gt;6 levels of knowledge management maturity in organizations&lt;/h1&gt;
&lt;p&gt;&amp;ldquo;Zapomniałem&amp;rdquo; is Polish for &amp;ldquo;I forgot&amp;rdquo;.  &lt;/p&gt;

&lt;p&gt;On Arkency&amp;rsquo;s Slack, our main communication channel, it has been used over 1200 times.&lt;br&gt;
And I truly believe I work with exceptionally organized and meticulous people.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;That only confirms what I wrote in my &lt;a href="https://blog.arkency.com/maintaining-an-organizational-knowledge-graph-with-an-llm-and-event-sourcing/"&gt;previous post&lt;/a&gt;: &lt;em&gt;organizations are surprisingly good at forgetting&lt;/em&gt;.  &lt;/p&gt;

&lt;p&gt;That post described how we maintain an organizational knowledge graph with an LLM and event sourcing.&lt;br&gt;
It was about the destination.&lt;br&gt;
This one is about the road that led us there.&lt;/p&gt;

&lt;p&gt;Looking back at how we handle knowledge at Arkency, I identified 6 levels of maturity.&lt;br&gt;
I presented them yesterday at &lt;a href="https://programistok.org"&gt;Programistok&lt;/a&gt; in Białystok, and I am writing this post on the train back home.  &lt;/p&gt;

&lt;p&gt;Check where your organization stands.&lt;/p&gt;
&lt;h2 id="level_0__written_sources"&gt;Level 0: written sources&lt;/h2&gt;
&lt;p&gt;You communicate mostly via email and instant messengers.&lt;br&gt;
You record transcripts of your conversations.&lt;br&gt;
Agreements made on calls are confirmed in writing: someone posts a note stating what the parties have agreed on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This was our starting point&lt;/strong&gt;.&lt;br&gt;
Arkency embraced remote work long before it was cool, and the &lt;a href="https://blog.arkency.com/async-remote"&gt;async remote&lt;/a&gt; philosophy has always relied on written artifacts: status updates, meeting summaries, decision records.&lt;/p&gt;
&lt;h2 id="level_1__one_silo"&gt;Level 1: one silo&lt;/h2&gt;
&lt;p&gt;All of the above, and additionally, those written artifacts are not scattered across many tools.&lt;br&gt;
They land in a single silo.&lt;/p&gt;

&lt;p&gt;This is the stage at which you start transcribing meetings automatically and using an LLM to generate summaries.&lt;/p&gt;

&lt;p&gt;In our case, we use Slack, and we started recording our meetings on Zoom.&lt;br&gt;
Configuring a Zapier automation that generates a summary of each meeting and posts it to the right channel took literally 15 minutes, including writing the prompt.&lt;br&gt;
The prompt is 30 lines long.&lt;br&gt;
&lt;strong&gt;The cost of reaching this level was close to zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you know what you are looking for, you will usually find it.&lt;br&gt;
Sometimes only because you remember a specific word that was said.&lt;/p&gt;
&lt;h2 id="level_2__semantic_search"&gt;Level 2: semantic search&lt;/h2&gt;
&lt;p&gt;All of the above, and additionally, you can search your written sources semantically.&lt;br&gt;
In practice, you no longer need to remember that specific word to find the right piece of data.&lt;/p&gt;

&lt;p&gt;At this level, an embedding model turns chunks of your documents into vectors: coordinates on a map of meanings.&lt;br&gt;
When a question comes in, the most semantically similar chunks are put into the LLM&amp;rsquo;s context, so it can answer questions about data it was never trained on.&lt;br&gt;
This technique is called &lt;a href="https://arxiv.org/abs/2005.11401"&gt;RAG&lt;/a&gt; (Retrieval-Augmented Generation).&lt;br&gt;
It is hardly news anymore - the idea dates back to 2020 and has been widely adopted since 2023.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For many companies, level 2 is perfectly enough.&lt;/strong&gt;&lt;br&gt;
If you want to build chatbots or virtual assistants answering specific kinds of questions (customer support, onboarding based on product documentation, policies and regulations), you don&amp;rsquo;t need to go further.&lt;br&gt;
There is one condition though: the questions have to be &lt;strong&gt;local&lt;/strong&gt;.&lt;br&gt;
The answer has to sit in a single chunk, not be smeared across the whole dataset.&lt;br&gt;
And the documents should be independent, not describing a network of dependencies, hierarchies or sequences in time.&lt;/p&gt;
&lt;h2 id="level_3__the_llm_organizes_knowledge"&gt;Level 3: the LLM organizes knowledge&lt;/h2&gt;
&lt;p&gt;All of the above, and additionally, you use an LLM to create syntheses of your documents.&lt;br&gt;
Knowledge about your organization now lives outside the original sources.&lt;br&gt;
Unstructured data becomes semi-structured.&lt;/p&gt;

&lt;p&gt;A perfect example of this level is the &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f"&gt;LLM Wiki&lt;/a&gt; proposed by Andrej Karpathy in April 2026.&lt;br&gt;
His starting point: RAG is in fact a search engine.&lt;br&gt;
It does not help to organize knowledge in a durable way - the synthesis happens from scratch with every question.&lt;br&gt;
Instead, he suggests letting an LLM maintain a wiki of interlinked markdown files.&lt;/p&gt;

&lt;p&gt;Traditional wikis die because the cost of maintaining them grows faster than the value they bring, and at some point nobody updates them anymore.&lt;br&gt;
A model doesn&amp;rsquo;t feel that cost.&lt;br&gt;
It doesn&amp;rsquo;t get bored rewriting the same page for the twentieth time.&lt;/p&gt;

&lt;p&gt;You get not only a semantic index, but also a useful artifact that both humans and agents can browse.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f"&gt;comments under the gist&lt;/a&gt; point out its weak spots, and the next levels are largely about addressing them.&lt;/p&gt;
&lt;h2 id="level_4__a_knowledge_graph"&gt;Level 4: a knowledge graph&lt;/h2&gt;
&lt;p&gt;All of the above, but this time the LLM turns unstructured data into formally structured data: a graph of entities and typed relations between them.&lt;/p&gt;

&lt;p&gt;The term was popularized by Google in 2012, when they published &lt;a href="https://blog.google/products-and-platforms/products/search/introducing-knowledge-graph-things-not/"&gt;&lt;em&gt;Introducing the Knowledge Graph: things, not strings&lt;/em&gt;&lt;/a&gt;.&lt;br&gt;
The search engine stopped merely matching strings and started recognizing entities.  &lt;/p&gt;

&lt;p&gt;In 2024, Microsoft stirred up the RAG world with &lt;a href="https://arxiv.org/abs/2404.16130"&gt;GraphRAG&lt;/a&gt;.&lt;br&gt;
They used an LLM to extract nodes and edges from chunks of text, then detected communities in the graph - groups of nodes more strongly connected with each other than with the rest.&lt;br&gt;
Each community gets a summary.&lt;br&gt;
When a question comes in, an LLM generates a partial answer from each community summary, filters out the unhelpful ones, and combines the rest into a final answer.&lt;br&gt;
This way, an LLM can answer &lt;strong&gt;global&lt;/strong&gt; questions, like &amp;ldquo;what are the main themes?&amp;rdquo; or &amp;ldquo;what trends can we see?&amp;rdquo;, where the answer is spread over a huge number of sources.&lt;br&gt;
Plain RAG cannot do that.&lt;/p&gt;

&lt;p&gt;Two things are crucial at this level: an ontology that frames the graph and cuts out the noise, and identity resolution that keeps the same entity from appearing as many nodes.&lt;br&gt;
I covered both in detail in the &lt;a href="https://blog.arkency.com/maintaining-an-organizational-knowledge-graph-with-an-llm-and-event-sourcing/"&gt;previous post&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="level_5__a_graph_you_can_rely_on"&gt;Level 5: a graph you can rely on&lt;/h2&gt;
&lt;p&gt;This is where your graph stops being a toy.&lt;br&gt;
After all, we don&amp;rsquo;t build a knowledge graph for the sake of having one, but for the automations it enables.&lt;br&gt;
At level 5, your agents work with the graph, traversing it while carrying out complex tasks.&lt;/p&gt;

&lt;p&gt;To let that happen, you need solid foundations.&lt;br&gt;
From my perspective, these are the must-haves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Human in the loop.&lt;/strong&gt; The LLM only proposes changes, and a human gets a chance to review them before they land in the graph.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full provenance.&lt;/strong&gt; Every fact traces back to its source, and every decision of the model traces back to what it read from the graph.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporal relations.&lt;/strong&gt; Relations between entities change over time, and two sources saying different things do not necessarily contradict each other. The graph should model time frames as attributes of relations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="where_are_you_"&gt;Where are you?&lt;/h2&gt;
&lt;p&gt;At the end of the talk, I asked the audience to raise their hands, level by level.&lt;br&gt;
When I asked who uses a knowledge graph in their company, only a few hands went up.&lt;/p&gt;

&lt;p&gt;Knowledge graphs as a foundation for AI agents are still a fresh topic.&lt;br&gt;
I&amp;rsquo;m currently reading &lt;em&gt;Agentic GraphRAG&lt;/em&gt; by Anthony Alcaraz and Sam Julien (O&amp;#39;Reilly) - its paperback edition comes out in just two days.&lt;br&gt;
It expands on much of what I described here as level 5.&lt;/p&gt;

&lt;p&gt;And if you want to move your organization up a level or two, &lt;a href="https://arkency.com/hire-us/"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Last but not least, big thanks to the organizers of &lt;a href="https://programistok.org"&gt;Programistok&lt;/a&gt; for a great conference and for having us in Białystok.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/6-levels-of-knowledge-management-maturity/20260926_173129.jpg?w=768&amp;h=758&amp;fit=max" width="100%"&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From the left: me, Tomek, Piotrek and Maciej.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:blog.arkency.com,2026-09-18:/teraz-live-collective-ambient-experience-made-with-rails-and-tone-dot-js/</id>
    <title type="html">TERAZ - live collective ambient experience made with Rails &amp; Tone.js</title>
    <published>2026-09-18T20:50:08Z</published>
    <updated>2026-09-18T20:50:08Z</updated>
    <author>
      <name>Maciej Korsan</name>
      <uri>https://blog.arkency.com/authors/maciej-korsan/</uri>
    </author>
    <link rel="alternate" href="https://blog.arkency.com/teraz-live-collective-ambient-experience-made-with-rails-and-tone-dot-js/" type="text/html"/>
    <content type="html">&lt;h1 id="teraz___live_collective_ambient_experience_made_with_rails__amp__tone_js"&gt;TERAZ - live collective ambient experience made with Rails &amp;amp; Tone.js&lt;/h1&gt;
&lt;p&gt;The first time TERAZ actually worked, there were around seventy phones in the audience at the wroclove.rb conference.&lt;/p&gt;

&lt;p&gt;Before pressing start, I was still not completely sure what would happen. Local tests are one thing. Seventy people with their own phones, batteries, browsers, volume settings, operating systems, and expectations are another.&lt;/p&gt;

&lt;p&gt;I was standing at my laptop, looking at a grid of rectangles on the screen, slowly bringing the instruments in. Each rectangle was one person from the audience. Each phone had been randomly assigned a part: bass, piano, crackling noise, angel pads, drone, or bells.&lt;/p&gt;

&lt;p&gt;Then the room started to sound like one strange, fragile ambient organism, and I got goosebumps.&lt;/p&gt;

&lt;iframe width="100%" height="315" src="https://www.youtube.com/embed/hT6QU2p8-AQ?si=oy-7QZajYfFnw8vC" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;
&lt;h2 id="why_i_made_teraz"&gt;Why I made TERAZ&lt;/h2&gt;
&lt;p&gt;I have been orbiting around ambient music for years - as a DJ, artist, listener, and organizer of Up To Date Festival in Białystok. It is close to my heart. Ambient has always made sense to me because it can be both background and the whole world, depending on how much attention you give it.&lt;/p&gt;

&lt;p&gt;At the same time, I spend a lot of my life in technology. I write code, build systems, and think about how things communicate with each other. For a long time, I wanted to make something where those two parts of me would actually meet, not as a gimmick, but as one idea. TERAZ came from that place.&lt;/p&gt;

&lt;p&gt;Let&amp;rsquo;s talk a bit about the name. TERAZ means NOW in Polish. For me, that word became the whole mechanism of the piece. Participants can say: NOW I am part of the orchestra, NOW I am listening to what is happening around me, NOW I might want to check my phone, take a photo, record something - but if I do, I fall out of the orchestra.&lt;/p&gt;
&lt;h2 id="the_phone_as_part_of_the_performance"&gt;The phone as part of the performance&lt;/h2&gt;
&lt;p&gt;I have been thinking for a long time about how we use phones during concerts and performances. Very often, instead of being fully in the room, we &amp;ldquo;document&amp;rdquo; the thing. We record, take photos, check notifications, look at something else for a second. A phone can pull us out of the exact thing we came to experience.&lt;/p&gt;

&lt;p&gt;With TERAZ, I wanted to turn that around - the phone stays in your hand, but its role changes. To remain in the piece, you have to keep touching the screen so it does not go to sleep. It is a very simple gesture, almost stupidly simple, but it works. You are holding the device that usually distracts you, and now that same device requires your attention in order to keep playing.&lt;/p&gt;

&lt;p&gt;That is why TERAZ has always felt a bit like a small manifesto to me. No slogans, no big statement from the stage - just a small rule: if you want to stay in the piece, stay with the phone. The technical challenge and the artistic idea grew together. The system exists to make that gesture possible.&lt;/p&gt;
&lt;h2 id="what_i_learned_from_pogwar"&gt;What I learned from Pogwar&lt;/h2&gt;
&lt;p&gt;A few years earlier, I made a generative ambient music performance called Pogwar, which I presented at Arsenał Gallery in Białystok. It was based on Tone.js and gave me a lot of space to explore browser-based generative sound.&lt;/p&gt;

&lt;p&gt;Some things from Pogwar came back later in TERAZ. One of them was a hack I discovered while working on that project: an audio element playing a silent MP3 file on an infinite loop, which helps JavaScript keep running in the background on mobile devices.&lt;/p&gt;

&lt;p&gt;I would not broadly recommend this trick. It does not run at full power and can introduce artifacts. But it taught me something about what mobile browsers allow, what they block, and where the weird edges are.&lt;/p&gt;
&lt;h2 id="how_teraz_works"&gt;How TERAZ works&lt;/h2&gt;
&lt;p&gt;From the audience side, it goes like this: at the beginning, I welcome everyone and explain the rules. During the premiere at wroclove.rb, I could have done this better. It was the first public version, and I learned very quickly that people need to understand one important thing: their phone may not start playing immediately. The orchestra is built gradually.&lt;/p&gt;

&lt;p&gt;After the intro, people scan a QR code shown on the main screen. The system randomly assigns each person one of six tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bass&lt;/li&gt;
&lt;li&gt;piano&lt;/li&gt;
&lt;li&gt;crackling noise&lt;/li&gt;
&lt;li&gt;angel pads&lt;/li&gt;
&lt;li&gt;drone&lt;/li&gt;
&lt;li&gt;bells&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I press start, the QR code screen changes into a visualization: a matrix of rectangles, each representing one participant. Every phone becomes one small block inside the larger organism.&lt;/p&gt;

&lt;p&gt;My control panel is somewhere between a mixer, a conductor’s desk, and a small emergency room.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/teraz-live-collective-ambient-experience/teraz-conductor.avif" class="w-full"/&gt;&lt;/p&gt;

&lt;p&gt;I can see the six tracks, each with a volume slider and controls for locking or unlocking the instrument. Below that, I can see the participants, their assigned instruments, and their operating systems. I can also manually change assignments, which turned out to be very useful - for example, Android and bass were not exactly best friends.&lt;/p&gt;

&lt;p&gt;On some Android devices, there was basically no sound below around 120 Hz. iPhones generally handled the sound much better and played more reliably. Because every instrument is technically available on every device, I could spot when an Android phone had been assigned bass and move it to another instrument.&lt;/p&gt;
&lt;h2 id="building_for_many_different_phones"&gt;Building for many different phones&lt;/h2&gt;
&lt;p&gt;The system is built with Ruby on Rails, Action Cable, Stimulus, and Hotwire. Sound generation happens in JavaScript with Tone.js.&lt;/p&gt;

&lt;p&gt;I did not stream audio to the phones. That felt too heavy and too fragile. Instead, each phone can play any of the instruments locally. The server only sends small control messages through Action Cable: this track is active, this is the current volume, something has changed.&lt;/p&gt;

&lt;p&gt;The piano is the one exception in the sound design. It uses a sample downloaded to the user&amp;rsquo;s phone, because generating a convincing piano directly on all those devices would be too computationally expensive. Everything else is generated on the participants’ phones.&lt;/p&gt;

&lt;p&gt;I was ready for more than 120 people. At home, I tested with phones borrowed from my wife and neighbors, creating a tiny domestic orchestra. I also tested with around 100 browser tabs in Chrome. This was funny, because my Mac gave up before the server did. I kept opening tabs, but only some of them were actually able to play sound. Others started hanging or failing because I was clogging up the local audio context. So I added special test parameters that skipped audio initialization and tested only the Action Cable communication.&lt;/p&gt;
&lt;h2 id="designing_sound_for_phone_speakers"&gt;Designing sound for phone speakers&lt;/h2&gt;
&lt;p&gt;The music did not start in code. At first, I used Ableton Move to search for sounds. I improvised a lot, trying to find textures that made sense for a room full of small speakers. When something felt right, I started translating it into code and rebuilding the idea as a Tone.js instrument.&lt;/p&gt;

&lt;p&gt;The six tracks have different roles: bass, piano, crackling noise, angel pads, drone, and bells. Together they create a slow ambient environment, but each participant only carries one part of it.&lt;/p&gt;

&lt;p&gt;But not everything is set in stone. I wanted to create controlled chaos. Each instrument moves somewhat randomly within a defined range. I did not want all phones to play perfectly evenly or identically. That would probably feel too clean. The small differences between devices, timing, and generated material give the whole thing a bit of life.&lt;/p&gt;

&lt;p&gt;At first, I thought I should make everything very precisely synchronized. Later, I let go of that idea. This is ambient music. Small timing differences are fine. They actually help. I did not measure latency in detail, because once I heard it in the room, it made sense.&lt;/p&gt;

&lt;p&gt;Tone.js is a great library, but you need to be careful with optimization, especially on weaker phones. Some ideas that worked nicely on my machine were too heavy or unreliable across devices. I had to simplify, remove, or limit some parts to keep compatibility as broad as possible.&lt;/p&gt;

&lt;p&gt;This was probably the biggest technical lesson for me: designing a real-time system for hundreds of phones is not the same as designing for a regular browser.&lt;/p&gt;
&lt;h2 id="what_i_would_change_next_time"&gt;What I would change next time&lt;/h2&gt;
&lt;p&gt;Before doing it again, I need to fix one very basic thing: the intro.&lt;/p&gt;

&lt;p&gt;People should know from the start that the piece grows slowly, and that waiting for your phone to enter is part of the experience. Otherwise, some participants expect everything to play immediately, because that is how most digital interfaces behave.&lt;/p&gt;

&lt;p&gt;I would love to show TERAZ in a more ambient or art-oriented context. I also want to try it at a larger scale. With this project, more phones really make a big difference.&lt;/p&gt;

&lt;p&gt;It could probably become an installation one day, but for now I am most interested in performing it live. The live version has tension. I can hear the room, see the system, feel the audience, and make decisions in the moment.&lt;/p&gt;

&lt;p&gt;From a technical perspective, I&amp;rsquo;d love to add Rails Event Store and create a performance timeline using the events, and also make a nice timeline visualization for RES itself.&lt;/p&gt;
&lt;h2 id="where_music_and_code_meet"&gt;Where music and code meet&lt;/h2&gt;
&lt;p&gt;I think TERAZ worked for me because I did not have to choose which hat to wear.&lt;/p&gt;

&lt;p&gt;The ambient-artistic part of me could care about slowness, texture, atmosphere, and attention. The programmer part could obsess over Action Cable, Android audio, sending as little data as possible, and keeping the thing alive on the cheapest Hetzner box.&lt;/p&gt;

&lt;p&gt;Somehow, for once, these were part of a single thing.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:blog.arkency.com,2026-08-26:/we-are-all-juniors-again/</id>
    <title type="html">We are all juniors again</title>
    <published>2026-08-26T14:40:52Z</published>
    <updated>2026-08-26T14:40:52Z</updated>
    <author>
      <name>Tomasz Stolarczyk</name>
      <uri>https://blog.arkency.com/authors/tomasz-stolarczyk/</uri>
    </author>
    <link rel="alternate" href="https://blog.arkency.com/we-are-all-juniors-again/" type="text/html"/>
    <content type="html">&lt;h1 id="we_are_all_juniors_again"&gt;We are all juniors again&lt;/h1&gt;
&lt;p&gt;A long, long time ago, when &lt;a href="https://en.akinator.com/"&gt;Akinator&lt;/a&gt; was the peak of &amp;ldquo;advanced AI&amp;rdquo; on the internet, before services became micro, and deployments meant sending a file directly from a local computer via FTP to a server, I was introduced to the Dreyfus Skill Model. As a junior dev back then, I liked it because it explained why my senior and architect colleagues could sometimes make impactful decisions intuitively, while I, on the contrary, was expecting step-by-step instructions on how to convert HTML form input into a database row.&lt;/p&gt;

&lt;p&gt;In short, the Dreyfus Skill Model describes that when learning a new skill, a learner goes through 5 stages to acquire it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Novice. We need/expect step-by-step instructions and rules here, and we have problems in situations that don&amp;rsquo;t follow them.&lt;/li&gt;
&lt;li&gt;Advanced beginner. We start to recognize some situation-specific nuances and use our experience instead of just rules.&lt;/li&gt;
&lt;li&gt;Competent. We consciously define goals and priorities and plan our actions according to the situation.&lt;/li&gt;
&lt;li&gt;Proficient. We intuitively recognize what the situation requires but consciously decide how to respond.&lt;/li&gt;
&lt;li&gt;Expert. We intuitively understand the situation and respond without consciously considering each step.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I&amp;rsquo;m pretty sure that depending on how long you&amp;rsquo;ve been in IT, you may find yourself in one of those stages.&lt;/p&gt;

&lt;p&gt;And now, getting back to AI and especially coding agents and support for the whole SDLC, it&amp;rsquo;s another skill, whether we want it or not, that we at least should know how to deal with. The good news is that whether you are a 20-, 30-, or 40-year veteran in the industry, or just joining, we are pretty much on the same level. This allows us not to know things, to experiment, to ask questions, to copy what works, and to change our views and opinions every week. This is what novices/beginners do, and I definitely feel I&amp;rsquo;m in those phases again. And I&amp;rsquo;m pretty sure that&amp;rsquo;s not only me.&lt;/p&gt;

&lt;p&gt;Surprisingly often, when digging deeper into those &amp;ldquo;high-level AI concepts&amp;rdquo;, conversations end up on something like &amp;ldquo;well, I have a skill/MD&amp;rdquo;, or &amp;ldquo;I have a nice prompt that does the job&amp;rdquo;. And that&amp;rsquo;s totally fine, as we are all learning how to use those tools and what works in the whole SDLC. What I&amp;rsquo;m trying to highlight is that instead of hearing about &amp;ldquo;10x productivity increase&amp;rdquo;, &amp;ldquo;building another harness&amp;rdquo;, &amp;ldquo;running a swarm of agents&amp;rdquo;, or &amp;ldquo;AI changing someone&amp;rsquo;s life&amp;rdquo;, I would actually want to hear how it&amp;rsquo;s done. With details. And those details may be a single line of text in CLAUDE.md or a short explanation of this &amp;ldquo;obviously known feature&amp;rdquo;, since people expect exactly this kind of detail, because everyone feels behind.&lt;/p&gt;

&lt;p&gt;We are in a place where, just a year ago, &lt;a href="https://survey.stackoverflow.co/2025/ai"&gt;the Stack Overflow Developer Survey 2025&lt;/a&gt; found that 76% of developers weren&amp;rsquo;t planning to use AI for deployment and monitoring, and 69% weren&amp;rsquo;t planning to use it for project planning. If that&amp;rsquo;s where we were as an industry a year ago, not many seniors around now 😉&lt;/p&gt;

&lt;p&gt;As IDEs improved over the years, they sped up our coding by providing autocompletion, code navigation, etc. With AI coding agents, it&amp;rsquo;s different — quite a lot of people say that our time will shift from coding to other activities. And ironically, I bet you&amp;rsquo;ve already seen projects that have done less to simplify monitoring and releases for their devs than they&amp;rsquo;re now doing for their machines.&lt;/p&gt;

&lt;p&gt;Building a harness? Show me if it&amp;rsquo;s just an instruction to your agent &amp;ldquo;ask user for review&amp;rdquo;, &lt;a href="https://code.claude.com/docs/en/hooks"&gt;Claude Code hooks&lt;/a&gt;, or maybe &lt;a href="https://blog.arkency.com/getting-nondeterministic-agent-into-deterministic-guardrails/"&gt;good old pre-commit hooks that you are sure will execute&lt;/a&gt; 😉&lt;/p&gt;

&lt;p&gt;Running your agent army? Let me know if those are your old laptops or VPSs and whether you tmux to them one by one, delegating some tasks, or maybe use &lt;a href="https://code.claude.com/docs/en/agent-teams"&gt;Claude&amp;rsquo;s Agent Teams&lt;/a&gt; or &lt;a href="https://ampcode.com/news/agents-in-orbs"&gt;AMP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Fewer slogans, more specifics.&lt;/p&gt;

&lt;p&gt;This is especially important now, when a lot changes literally every week, meaning there are new areas to discover and new ways to work with those tools. Their non-deterministic nature adds some complexity too. There are no best practices, so whatever works for you today is worth sharing. You don&amp;rsquo;t have to worry it will be outdated next month. You don&amp;rsquo;t have to worry about being wrong. On the other hand, it&amp;rsquo;s not out of the question that what you are doing now will stand the test of time and become a best practice sometime 🙂&lt;/p&gt;

&lt;p&gt;We are all juniors, and let&amp;rsquo;s have some fun with it!&lt;/p&gt;

&lt;p&gt;That said, and as this whole post was about sharing knowledge, here you have some links from us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.arkency.com/maintaining-an-organizational-knowledge-graph-with-an-llm-and-event-sourcing/"&gt;Piotr about maintaining an organizational knowledge graph with an LLM and event sourcing.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.arkency.com/getting-nondeterministic-agent-into-deterministic-guardrails/"&gt;Łukasz about getting a nondeterministic agent into deterministic guardrails.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=Mavtkt79lpA"&gt;Szymon on RBQ 2026 about mutation testing becoming critical in the era of AI agents.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.arkency.com/res-mcp-server/"&gt;Tomek about exploring the Event Store with res-mcp.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last but not least, if you prefer a condensed, one-to-one experience with Arkencers where we focus on your project and on making your team AI-native in the way we see it, you may be interested in this &lt;a href="https://arkency.com/ai-native-rails/"&gt;workshop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;See you next time, and remember: together we will all become seniors sometime 🙂&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:blog.arkency.com,2026-08-11:/maintaining-an-organizational-knowledge-graph-with-an-llm-and-event-sourcing/</id>
    <title type="html">Maintaining an organizational knowledge graph with an LLM and event sourcing</title>
    <published>2026-08-11T10:00:00Z</published>
    <updated>2026-08-11T10:00:00Z</updated>
    <author>
      <name>Piotr Jurewicz</name>
      <uri>https://blog.arkency.com/authors/piotr-jurewicz/</uri>
    </author>
    <link rel="alternate" href="https://blog.arkency.com/maintaining-an-organizational-knowledge-graph-with-an-llm-and-event-sourcing/" type="text/html"/>
    <content type="html">&lt;h1 id="maintaining_an_organizational_knowledge_graph_with_an_llm_and_event_sourcing"&gt;Maintaining an organizational knowledge graph with an LLM and event sourcing&lt;/h1&gt;
&lt;p&gt;Organizations are surprisingly good at forgetting.&lt;br&gt;
Decisions are made on calls, insights get buried in Slack threads, and a month later no one remembers why things are the way they are.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;Arkency is no exception.&lt;br&gt;
Weekly calls, ad-hoc meetings, our book clubs, Slack discussions, GitHub mentions, email inbox - we could use some support in organizing all those signals.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://rubycommunityconference.com"&gt;Ruby Community Conference 2026&lt;/a&gt; happened in March.&lt;br&gt;
In Kraków, &lt;strong&gt;Obie Fernandez&lt;/strong&gt; showed some parts of his NEXUS system.&lt;br&gt;
He had already described it &lt;a href="https://obie.medium.com/what-used-to-take-months-now-takes-days-cc8883cc21e9"&gt;on his blog&lt;/a&gt; back in January, but the conference was where I first came across it.&lt;br&gt;
That was the push I needed to start building our own software.&lt;/p&gt;

&lt;p&gt;When it was already taking shape, Andrej Karpathy published his &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f"&gt;LLM Wiki&lt;/a&gt; note.&lt;br&gt;
Instead of a RAG system rediscovering your documents on every query, an LLM incrementally maintains a persistent wiki: interlinked markdown pages, immutable sources underneath, and a human curating the loop.&lt;br&gt;
It was quite exciting to realize I was working on something that had just become one of the hottest topics in the industry.&lt;/p&gt;

&lt;p&gt;We ended up with &lt;em&gt;Planet Arkency&lt;/em&gt; - a multi-tenant &lt;strong&gt;knowledge graph with a closed ontology&lt;/strong&gt;, built on Rails Event Store.&lt;br&gt;
In this post, I want to walk you through the design decisions I made.&lt;/p&gt;
&lt;h2 id="unstructured_input_is_where_llms_actually_shine"&gt;Unstructured input is where LLMs actually shine&lt;/h2&gt;
&lt;p&gt;For structured data, you could have built such a system like twenty years ago.&lt;br&gt;
Webhooks, forms, integrations - parsing structured input into a graph is a solved problem.&lt;br&gt;
But the most interesting knowledge lives in the input no parser could ever handle: meeting transcripts, Slack discussions, emails, or anything coming from an integration nobody has built yet.&lt;br&gt;
This is where LLMs changed the game for us.&lt;/p&gt;

&lt;p&gt;Everything flows into the system through a single ingestion endpoint.&lt;br&gt;
Transcripts, Slack threads someone flagged with a dedicated emoji reaction, emails arriving at a bridge inbox, RSS feeds, calendar invites, personal notes.&lt;br&gt;
&lt;strong&gt;We don&amp;rsquo;t even write code for the integration points.&lt;/strong&gt;&lt;br&gt;
Tools like Zapier or n8n watch the sources and push the content to that single endpoint.&lt;/p&gt;

&lt;p&gt;Every ingested piece of content then goes through an &lt;strong&gt;extraction&lt;/strong&gt; - the heart of the system.&lt;br&gt;
An LLM reads the content and works out what it means for our knowledge: which entities appear in it, what we learned about them, and how they relate to each other.&lt;br&gt;
Most of this post is about what happens around that single step.&lt;/p&gt;
&lt;h2 id="why_a_graph_"&gt;Why a graph?&lt;/h2&gt;
&lt;p&gt;The same names keep coming back in our conversations: people, projects, clients, tools, decisions.&lt;br&gt;
What changes from week to week is what we know about them and how they relate to each other.&lt;br&gt;
That maps naturally to a graph: entities with attributes, connected by typed relations.&lt;br&gt;
Who works on what.&lt;br&gt;
Who made which decision, and when.&lt;br&gt;
Which project depends on which tool.&lt;/p&gt;

&lt;p&gt;This is where we differ most from the LLM Wiki approach.&lt;br&gt;
In a wiki, the fact that someone works on some project is written down in a sentence on a page, at best with a link between the two pages.&lt;br&gt;
The knowledge is there, but only a reader can make use of it.&lt;br&gt;
In a typed graph, &lt;code&gt;person --works_on--&amp;gt; project&lt;/code&gt; is a piece of data: you can query it, traverse it, count it.&lt;/p&gt;

&lt;p&gt;The graph itself sits on PostgreSQL: a &lt;code&gt;nodes&lt;/code&gt; table, an &lt;code&gt;edges&lt;/code&gt; table with a unique &lt;code&gt;(source, target, relation)&lt;/code&gt; triple, &lt;code&gt;jsonb&lt;/code&gt; attributes on both - no rocket science here.&lt;br&gt;
Dedicated graph databases (Neo4j, triple stores like the one NEXUS uses) could be a better fit for some specific workloads, like deep multi-hop traversal.&lt;br&gt;
But nothing above is Postgres-specific.&lt;br&gt;
A schema this plain is what makes the storage an actual detail - there is little to port when you write another adapter for the data layer.&lt;/p&gt;
&lt;h3 id="the_ontology"&gt;The ontology&lt;/h3&gt;
&lt;p&gt;Which kinds of nodes and relations may exist is defined in an &lt;strong&gt;ontology&lt;/strong&gt;, stored in a plain YAML file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# from config/ontology.yml&lt;/span&gt;
&lt;span class="na"&gt;node_kinds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;person&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;member,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;candidate,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;client&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;contact,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;external&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;person"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;decision&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;requiring&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;casual&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;suggestions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;idea"&lt;/span&gt;
&lt;span class="na"&gt;edge_relations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;works_on&lt;/span&gt;
    &lt;span class="na"&gt;signature&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;person&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--works_on--&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;project"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The ontology is closed - if a kind or relation is not on the list, the model cannot use it.&lt;br&gt;
Initially I was thinking about an open ontology, where the LLM could introduce its own types.&lt;br&gt;
It brought complete chaos into the graph surprisingly fast.&lt;br&gt;
In my opinion, it is better to tell the model upfront what to look for.&lt;/p&gt;
&lt;h3 id="not_one_graph__but_many"&gt;Not one graph, but many&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;The organizational knowledge graph&amp;rdquo;&lt;/em&gt; suggests one universal graph for all different purposes.&lt;br&gt;
We don&amp;rsquo;t believe in that, and DDD practitioners will recognize why.&lt;/p&gt;

&lt;p&gt;We use a multi-tenant architecture to maintain separate graphs with their own ontologies, which really means their own ubiquitous languages.&lt;br&gt;
Our internal Arkency graph speaks in people, projects and decisions - a domain quite close to a CRM.&lt;br&gt;
The graph we run as Rails Event Store maintainers speaks in releases, known problems and community content:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/arkency-planet/ontology.png" width="100%"&gt;&lt;/p&gt;

&lt;p&gt;Different domains, different vocabularies, the same machinery underneath.&lt;br&gt;
The boundaries of a bounded context tell you where one graph ends and another begins.&lt;/p&gt;
&lt;h2 id="what_comes_out_of_an_extraction"&gt;What comes out of an extraction&lt;/h2&gt;
&lt;p&gt;The ontology is rendered into the extraction prompt as markdown tables and into the schema as enums.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;(from app/lib/prompts/extraction.md.erb)

You are an organizational knowledge analyst for &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="no"&gt;Tenancy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;. We are building an internal knowledge graph.

Extract a knowledge graph from the provided content: nodes and edges. The graph should allow full reconstruction of the provided content.

## Nodes

Each node has: name, kind, short_description, description, attrs (optional key-value pairs).

Allowed kinds:

| kind | what it represents | typical attrs |
|---|---|---|
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;ontology&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;node_kinds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;
| &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; | &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; | &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"attrs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"—"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; |
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;

## Edges

Each edge has: source, target, relation, context, attrs (optional key-value pairs).

Allowed relations:

| relation | source kind | target kind | hint | attrs |
|---|---|---|---|---|
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;ontology&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edge_relations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt;
  &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ontology&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;source_kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:source&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" / "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;target_kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:target&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" / "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"—"&lt;/span&gt;
  &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"attrs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"—"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;
| &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; | &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;source_kind&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; | &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;target_kind&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; | &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; | &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; |
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;-%&amp;gt;&lt;/span&gt;

...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each extraction ends with the model returning &lt;strong&gt;one structured result&lt;/strong&gt;: the entities it found in the content, the relations between them, and how the existing graph should change to reflect them.&lt;br&gt;
We use &lt;a href="https://rubyllm.com/chat/#using-rubyllmschema-recommended"&gt;RubyLLM&amp;rsquo;s schema support&lt;/a&gt; for that.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# from app/lib/extraction_result_schema.rb&lt;/span&gt;
&lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="ss"&gt;:nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Entities to create or update. Each name must be unique — no duplicate nodes."&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;enum: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"existing"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"'existing' iff the node was returned by search_nodes/list_nodes_by_kind/get_node_edges and you are reusing it. 'new' if you are introducing it. The system verifies the canonical name and aborts on mismatch."&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Entity name. For 'existing' nodes use the EXACT canonical name from the tool call result. For 'new' nodes the canonical name you are introducing."&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:new_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Optional. Set ONLY for 'existing' nodes when the content reveals a more explicit canonical form (e.g. acronym → full term, diminutive → full name). The node is looked up by `name` and renamed to `new_name`."&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Must be one of: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;kind_names&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:short_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Stable synthesis of what this entity is (for search). General and identity-focused, not episode-specific. Max 15 words."&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"For new nodes: brief description based on the content. For existing nodes: synthesize prior description with new information. Rewriting for clarity is fine, but preserve prior facts."&lt;/span&gt;
    &lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="ss"&gt;:attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Key-value attributes. Only include what is known from the content."&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="ss"&gt;:aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Optional. Alternative surface forms (diminutives, acronyms, full vs short forms) under which this entity was referred to in the content, or — when renaming via `new_name` — the old canonical if it remains a valid surface form. Only include NEW aliases not already present on the existing node. An alias is the SAME entity under another name — never a separate entity."&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="ss"&gt;:edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"ALL relationships. Be thorough and precise."&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Source node name (exact match — existing or newly created)"&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Target node name (exact match — existing or newly created)"&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:relation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Must be one of: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;relation_names&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Briefly explain why this relationship exists, grounded in the content"&lt;/span&gt;
    &lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="ss"&gt;:attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Key-value attributes for this edge (e.g. since, weight)"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Actual data operations (create or update, with the exact field-level diff) are derived server-side.&lt;br&gt;
We load or initialize an ActiveRecord model, assign what the LLM returned, and let dirty tracking do the rest:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# from app/handlers/propose_graph_change.rb&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_or_initialize_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;enforce_status!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# raises when the model's new/existing claim disagrees with the DB&lt;/span&gt;

&lt;span class="n"&gt;was_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_record?&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;short_description: &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attrs: &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;op: &lt;/span&gt;&lt;span class="n"&gt;was_new&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"create"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;node_id: &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persisted?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;changes: &lt;/span&gt;&lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;node.changes&lt;/code&gt; gives us &lt;code&gt;{field =&amp;gt; [before, after]}&lt;/code&gt; pairs for free, and this before/after snapshot becomes the wire format of the graph change proposal.&lt;br&gt;
Edges get exactly the same treatment - looked up by their &lt;code&gt;(source, target, relation)&lt;/code&gt; triple and diffed with dirty tracking.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/arkency-planet/extraction-result.png" width="100%"&gt;&lt;/p&gt;

&lt;p&gt;We also don&amp;rsquo;t blindly trust what the LLM claims.&lt;br&gt;
It has to declare each node as &lt;code&gt;new&lt;/code&gt; or &lt;code&gt;existing&lt;/code&gt;, and a validator cross-checks it against the database.&lt;br&gt;
On mismatch, the LLM gets natural-language feedback and another attempt on the same conversation.&lt;/p&gt;
&lt;h2 id="identity_resolution_is_the_hard_part"&gt;Identity resolution is the hard part&lt;/h2&gt;
&lt;p&gt;I just wrote that the model has to declare each node as &lt;code&gt;new&lt;/code&gt; or &lt;code&gt;existing&lt;/code&gt;.&lt;br&gt;
But how would it know?&lt;br&gt;
Do we load the whole graph into LLM context?&lt;br&gt;
No - this is where &lt;strong&gt;tool calls&lt;/strong&gt; come in.&lt;/p&gt;

&lt;p&gt;And it is harder than a simple lookup.&lt;br&gt;
&amp;ldquo;Piotrek&amp;rdquo;, &amp;ldquo;Piotr Jurewicz&amp;rdquo; and whatever Zoom&amp;rsquo;s transcription makes out of my name are the same person.&lt;br&gt;
If you create a node per surface form, your graph turns into garbage within a week.&lt;/p&gt;

&lt;p&gt;We handle it on three levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the model must look before it writes.&lt;/strong&gt;&lt;br&gt;
During extraction it has access to read-only tools like &lt;code&gt;search_nodes&lt;/code&gt; or &lt;code&gt;get_node_edges&lt;/code&gt;.&lt;br&gt;
The extraction prompt is explicit about it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(from app/lib/prompts/extraction.md.erb)
- Before creating any node, use search_nodes to check if it already exists. (...)
- If search_nodes returns no results, the node does not exist yet — proceed to create it. (...)
- If search_nodes returns ambiguous results, or you need broader context to make extraction decisions, use get_node_edges to inspect the node's connections.
- After finding nodes with search_nodes, use get_node_edges to see their existing relationships before deciding how to connect them.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Second, aliases are the identity mechanism.&lt;/strong&gt;&lt;br&gt;
Each node has one canonical name and any number of aliases.&lt;br&gt;
The schema instructs the model that an alias is the same entity under another name - never a separate entity.&lt;br&gt;
When the content reveals a better canonical form, the model sets &lt;code&gt;new_name&lt;/code&gt; and the old name stays as an alias, so future fuzzy searches still resolve it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, the search is hybrid.&lt;/strong&gt;&lt;br&gt;
Trigram similarity (pg_trgm with GIN indexes) over node names &lt;em&gt;and&lt;/em&gt; aliases catches misspellings.&lt;br&gt;
Embedding search catches semantic matches which share no characters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# from app/models/node.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hybrid_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# fuzzy match on canonical names and aliases, powered by pg_trgm&lt;/span&gt;
  &lt;span class="n"&gt;by_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"similarity(nodes.name, ?) &amp;gt; 0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;by_alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:aliases&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"similarity(node_aliases.name, ?) &amp;gt; 0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;trigram_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;union_by_best_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;by_alias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"bge-m3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;provider: :ollama&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;semantic_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;nearest_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;distance: &lt;/span&gt;&lt;span class="s2"&gt;"cosine"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;neighbor_distance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;SEMANTIC_THRESHOLD&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;merge_and_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trigram_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;semantic_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The embeddings come from a self-hosted &lt;code&gt;bge-m3&lt;/code&gt; model on Ollama, stored in pgvector.&lt;/p&gt;
&lt;h2 id="every_fact_has_a_source"&gt;Every fact has a source&lt;/h2&gt;
&lt;p&gt;A graph edited by an AI is only trustworthy if you can audit every change.&lt;br&gt;
For every node and edge we can answer: which extraction created you, which extractions updated you, and what exactly changed each time.&lt;/p&gt;

&lt;p&gt;Provenance lives in join tables (&lt;code&gt;node_extractions&lt;/code&gt; and &lt;code&gt;edge_extractions&lt;/code&gt;): one row per extraction and entity pair, holding the operation, the status, and the field-level &lt;code&gt;diff&lt;/code&gt; produced by the dirty tracking described before.&lt;br&gt;
Starting from any node, you can walk back through these rows to the extraction that touched it, and from the extraction to the ingested content it was based on.&lt;br&gt;
Every fact in the graph traces back to its source.&lt;/p&gt;

&lt;p&gt;We also record something we call the &lt;strong&gt;read set&lt;/strong&gt;.&lt;br&gt;
Every tool call the model makes during extraction is published as an &lt;code&gt;ExtractionToolCalled&lt;/code&gt; event and projected into &lt;code&gt;tool_invocations&lt;/code&gt;, linked to the nodes and edges the call returned.&lt;br&gt;
So we know not only what an extraction wrote, but also what it read before deciding.&lt;br&gt;
When you wonder &amp;ldquo;why did the model merge these two people?&amp;rdquo;, the answer is on the extraction page: here is the search it ran, and here is what came back.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/arkency-planet/tool-calls.png" width="100%"&gt;&lt;/p&gt;

&lt;p&gt;Each node&amp;rsquo;s page shows its full history: created in, last updated in, read by N extractions.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/arkency-planet/provenance.png" width="100%"&gt;&lt;/p&gt;
&lt;h2 id="keeping_an_eye_on_the_costs"&gt;Keeping an eye on the costs&lt;/h2&gt;
&lt;p&gt;Besides auditing changes in the graph, we also track how much each extraction costs: token usage and the resulting price.&lt;br&gt;
When you work with an LLM API, it is worth keeping a finger on the pulse here.&lt;br&gt;
A transcript of a few hours of conversation, processed in multiple rounds interleaved with tool calls, can generate significant costs.&lt;br&gt;
&lt;a href="https://rubyllm.com/chat/#anthropic-prompt-caching"&gt;Prompt caching&lt;/a&gt; helps a lot - the system prompt and the content stay identical between rounds, so most of the input is billed at the cache-read rate.&lt;/p&gt;

&lt;p&gt;The exact numbers depend on the model you run the extraction on, but most of ours cost well under a dollar.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/arkency-planet/extractions-cost.png" width="100%"&gt;&lt;/p&gt;
&lt;h2 id="human_in_the_loop"&gt;Human in the loop&lt;/h2&gt;
&lt;p&gt;We don&amp;rsquo;t let the LLM write to the graph directly.&lt;br&gt;
Extraction produces a &lt;strong&gt;proposal&lt;/strong&gt; with the before/after diffs, and applying it to the graph is a separate step.&lt;/p&gt;

&lt;p&gt;Proposals can sit in a review window before they get applied.&lt;br&gt;
As soon as an extraction completes, we get a short summary of it on Slack.&lt;br&gt;
A human can inspect the diff, apply it early, or just let it flow after the configured delay.&lt;/p&gt;

&lt;p&gt;Time passes between propose and apply, so the graph may have moved in the meantime.&lt;br&gt;
When the current state no longer matches what the proposal was based on, the apply stops and the affected rows get marked as conflicted, with a human-readable explanation.&lt;/p&gt;
&lt;h2 id="event_sourcing_ties_it_all_together"&gt;Event sourcing ties it all together&lt;/h2&gt;
&lt;p&gt;You may have noticed that every mechanism above was described in terms of events.&lt;br&gt;
Well, this is an Arkency blog after all.&lt;/p&gt;

&lt;p&gt;The whole pipeline is an event flow: &lt;code&gt;TranscriptIngested&lt;/code&gt; → &lt;code&gt;ExtractionRequested&lt;/code&gt; → &lt;code&gt;KnowledgeExtracted&lt;/code&gt; → &lt;code&gt;GraphChangeProposed&lt;/code&gt; → &lt;code&gt;GraphChangeApplied&lt;/code&gt; (or &lt;code&gt;GraphChangeConflicted&lt;/code&gt;).&lt;br&gt;
Two small aggregates guard the invariants: one per ingestion (no two concurrent extractions of the same content), one per extraction (the propose → apply state machine).&lt;br&gt;
Everything you see in the UI (ingestions, extractions, diffs, tool invocations) is a read model built from these events.&lt;/p&gt;

&lt;p&gt;In this architecture, the review window is just one more state in the aggregate&amp;rsquo;s state machine, and provenance is just one more read model built from an event we already had.&lt;br&gt;
I cannot understand people claiming that event sourcing makes things more complex ;)&lt;/p&gt;
&lt;h2 id="the_graph_can_feed_itself"&gt;The graph can feed itself&lt;/h2&gt;
&lt;p&gt;One feature shows the value of a uniform pipeline well.&lt;br&gt;
From any node you can request research.&lt;br&gt;
A job asks a model equipped with Anthropic&amp;rsquo;s server-side &lt;code&gt;web_search&lt;/code&gt; and &lt;code&gt;web_fetch&lt;/code&gt; tools to compile a brief about the entity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# from app/jobs/research_topic.rb&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="no"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"web_search_20250305"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"web_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_uses: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"web_fetch_20250910"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"web_fetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_uses: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ResearchBriefSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The prompt grounds the research in what the graph already knows about the entity, and tells the model when to give up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from app/jobs/research_topic.rb
Research "#{topic}". Use web_search and web_fetch as needed to gather facts.

In our knowledge base this entity is currently described as:
- Kind: ...
- Short description: ...
- Attributes: ...

When you can produce a useful brief, return status="completed" and put the
brief in `brief` as Markdown. (...) Cover identity, key facts a knowledgeable
reader should know, recent activity worth recording, and relationships to
other named entities. Include source URLs inline next to claims that come
from a specific page. Keep it factual; do not speculate.

Return status="aborted" instead — with `abort_reason` naming the specific
problem — when any of these holds:
- The topic is ambiguous and you cannot confidently pick the intended
  interpretation from the disambiguation context above.
- You cannot find substantive, verifiable information about this exact
  entity (...)

Do not pad an aborted result with related-but-different information.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The resulting brief is not applied to the graph directly.&lt;br&gt;
It gets published as a regular &lt;code&gt;TranscriptIngested&lt;/code&gt; event with its own kind, and flows through the same extraction, proposal and review pipeline as any other input.&lt;/p&gt;
&lt;h2 id="the_graph_speaks_mcp"&gt;The graph speaks MCP&lt;/h2&gt;
&lt;p&gt;The graph is not locked inside its own UI.&lt;br&gt;
We expose it over &lt;strong&gt;MCP&lt;/strong&gt;, so any AI assistant with access to our server can search it by asking questions in natural language - and answer from the graph, with sources.&lt;/p&gt;
&lt;h2 id="final_thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;Working on Planet Arkency taught me a lot.&lt;br&gt;
About graphs, about LLMs, and about concepts I had never even heard of before: ontologies, identity resolution, provenance.&lt;br&gt;
I hope some of that knowledge stays with you after reading this post.&lt;/p&gt;

&lt;p&gt;It also reassured me about the tools we have been using at Arkency for years.&lt;br&gt;
Event-driven architecture and Rails Event Store carried this project naturally.&lt;/p&gt;

&lt;p&gt;At the time of writing, the main production Arkency graph holds almost 2000 nodes and over 5200 edges, built by around 300 extractions.&lt;br&gt;
And behind all of that, over 3600 events recording how every single fact got there.&lt;/p&gt;

&lt;p&gt;I still have a head full of ideas on where to take this project next.&lt;/p&gt;

&lt;p&gt;Working with &lt;a href="https://rubyllm.com"&gt;RubyLLM&lt;/a&gt; was a pure pleasure - credits to Carmine Paolino for this gem.&lt;/p&gt;

&lt;p&gt;If you are thinking about organizational memory for your company, or want us to help you build one, &lt;a href="https://arkency.com/hire-us/"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:blog.arkency.com,2026-07-16:/3-example-process-managers-in-rails/</id>
    <title type="html">3 example process managers in Rails</title>
    <published>2026-07-16T09:03:31Z</published>
    <updated>2026-07-16T09:03:31Z</updated>
    <author>
      <name>Andrzej Krzywda</name>
      <uri>https://blog.arkency.com/authors/andrzej-krzywda/</uri>
    </author>
    <link rel="alternate" href="https://blog.arkency.com/3-example-process-managers-in-rails/" type="text/html"/>
    <content type="html">&lt;h1 id="3_example_process_managers_in_rails"&gt;3 example process managers in Rails&lt;/h1&gt;
&lt;p&gt;I am writing this blogpost from this beautiful place in the Polish mountains. Our RailsEventStore camp takes place this week. &lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog-arkency.imgix.net/3-example-process-managers/IMG_9894.jpeg?w=768&amp;h=758&amp;fit=max" width="70%"&gt;&lt;/p&gt;

&lt;p&gt;Process managers are meant to map business process into code. The more readable and declarative it is, the better the chance domain experts will understand it.&lt;/p&gt;

&lt;p&gt;Over the last years, we have experimented with several approaches on how to implement process managers in Ruby.&lt;/p&gt;

&lt;p&gt;The foundation here is &lt;a href="https://railseventstore.org"&gt;RailsEventStore&lt;/a&gt; and the architecture where events are published and commands are used to tell what to do next.&lt;/p&gt;

&lt;p&gt;Most business processes can be mapped into some sort of a checklist of requirements and when certain conditions are met we decide (a command) what to do next.&lt;/p&gt;

&lt;p&gt;During the RESCamp I have released a small library - &lt;a href="https://github.com/RailsEventStore/rails_event_store/tree/master/contrib/ruby_event_store-process_manager"&gt;ruby_event_store-process_manager&lt;/a&gt; - which was extracted from the ecommerce project.&lt;/p&gt;
&lt;h2 id="1__releasepaymentonorderexpiration"&gt;1. ReleasePaymentOnOrderExpiration&lt;/h2&gt;
&lt;p&gt;I will start with a process which defines what to do when a payment was authorized but the offer has expired. You can imagine that different businesses have different rules for such situations. BTW, that&amp;rsquo;s why I like to put process managers at the application layer and keep them under app/processes.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Processes&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReleasePaymentOnOrderExpiration&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProcessManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;ProcessState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;subscribes_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Payments&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PaymentAuthorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;Payments&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PaymentReleased&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OrderRegistered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OfferExpired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OrderConfirmed&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;act&lt;/span&gt;
      &lt;span class="n"&gt;release_payment&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release?&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Payments&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PaymentAuthorized&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;payment_authorized: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Payments&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PaymentReleased&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;payment_authorized: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OfferExpired&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;order_expired: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release_payment&lt;/span&gt;
      &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Payments&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ReleasePayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;order_id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="no"&gt;ProcessState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:payment_authorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:order_expired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;payment_authorized: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;order_expired: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release?&lt;/span&gt;
        &lt;span class="n"&gt;payment_authorized&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;order_expired&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thanks to the ProcessManager certain things are taken care of for free. One of them is the concept of streams and event sourcing. All events which are relevant to this process are grouped into one stream. This stream by convention will be called &lt;code&gt;ReleasePaymentOnOrderExpiration$uuid&lt;/code&gt; but it&amp;rsquo;s easy to override. All the events will be linked to this stream automatically. Thanks to this, the process will be rebuilt by replaying the stream on every new event.
Additionally, if two events for the same process race and hit a version conflict, the state-building is retried once.&lt;/p&gt;

&lt;p&gt;That&amp;rsquo;s what is hidden. &lt;/p&gt;

&lt;p&gt;What we have here is typical sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;events&lt;/li&gt;
&lt;li&gt;state building&lt;/li&gt;
&lt;li&gt;decision (&lt;code&gt;act&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2__reservation"&gt;2. Reservation&lt;/h2&gt;
&lt;p&gt;Reservation is a common problem to solve in order-related apps. However, every business defines their reservation process differently. 
Imagine a business which sells equipment around a specific topic, let&amp;rsquo;s say photography. You want to buy a camera, a lens and a tripod. All of this is important and if any of them is not available the whole order doesn&amp;rsquo;t make sense -  a camera the customer can&amp;rsquo;t yet use isn&amp;rsquo;t worth shipping&lt;/p&gt;

&lt;p&gt;This business process is implemented here:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Processes&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReservationProcess&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProcessManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;ProcessState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;subscribes_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OfferAccepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OrderCancelled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OrderConfirmed&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;act&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="ss"&gt;order: :accepted&lt;/span&gt;
        &lt;span class="n"&gt;unavailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reserve_stock&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unavailable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt;
          &lt;span class="n"&gt;reject_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unavailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="n"&gt;accept_order&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="ss"&gt;order: :cancelled&lt;/span&gt;
        &lt;span class="n"&gt;release_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserved_product_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="ss"&gt;order: :confirmed&lt;/span&gt;
        &lt;span class="n"&gt;dispatch_stock&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OfferAccepted&lt;/span&gt;
        &lt;span class="n"&gt;order_lines_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order_lines&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:product_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:quantity&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;order: :accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;order_lines: &lt;/span&gt;&lt;span class="n"&gt;order_lines_hash&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OrderCancelled&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;order: :cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OrderConfirmed&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;order: :confirmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reserve_stock&lt;/span&gt;
      &lt;span class="n"&gt;unavailable_products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="n"&gt;reserved_products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;product_id: &lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;reserved_products&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;
      &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InventoryEntry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InventoryNotAvailable&lt;/span&gt;
        &lt;span class="n"&gt;unavailable_products&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unavailable_products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt;
        &lt;span class="n"&gt;release_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reserved_products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="n"&gt;unavailable_products&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;product_ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;product_id: &lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch_stock&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;product_id: &lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accept_order&lt;/span&gt;
      &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Fulfillment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RegisterOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;order_id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reject_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unavailable_product_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Pricing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RejectOffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;order_id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;reason: &lt;/span&gt;&lt;span class="s2"&gt;"Some products were unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unavailable_product_ids&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="no"&gt;ProcessState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:order_lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;order: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;order_lines: &lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;order_lines: &lt;/span&gt;&lt;span class="n"&gt;order_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reserved_product_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What is worth noting here is that this business process can result in 5 different commands for 3 different contexts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Pricing::RejectOffer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fulfillment::RegisterOrder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Inventory::Dispatch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Inventory::Reserve&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Inventory::Release&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is very typical case in complex businesses. A context can be seen as a department in a company - that&amp;rsquo;s one of the DDD metaphors which I enjoy using.&lt;/p&gt;
&lt;h2 id="3__publishing_a_post_on_twitter_like_app"&gt;3. Publishing a post on Twitter-like app&lt;/h2&gt;
&lt;p&gt;This process comes from a different app. Imagine a social media app, where people see their timeline consisting of posts from the people they follow. The process here is simple - make a delivery (posting to a timeline) per each follower, but also include the author timeline.
In the future, we can imagine other rules. For example, we may want to decide to deliver to the online people first. Or maybe we want to push some ads to the timeline when a post seems related. It&amp;rsquo;s nice to have such business rules in one place.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TimelineDeliveryProcess&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;RubyEventStore&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ProcessManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;ProcessState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;subscribes_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UserFollowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UserUnfollowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostPublished&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;act&lt;/span&gt;
    &lt;span class="n"&gt;recipients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;recipient_id&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;deliver_post_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipient_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recipients&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;followers&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author_id&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UserFollowed&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;followers: &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;followers&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:follower_id&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UserUnfollowed&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;followers: &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;followers&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:follower_id&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostPublished&lt;/span&gt;
      &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UserFollowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UserUnfollowed&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:followee_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PostPublished&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver_post_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipient_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;command_bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Social&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeliverPostToTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;post_id: &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:post_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;recipient_id: &lt;/span&gt;&lt;span class="n"&gt;recipient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;author: &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;body: &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="no"&gt;ProcessState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:followers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;followers: &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;followers: &lt;/span&gt;&lt;span class="n"&gt;followers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the_ruby_event_store_process_manager_gem"&gt;The ruby_event_store-process_manager gem&lt;/h2&gt;
&lt;p&gt;As you see, &lt;a href="https://railseventstore.org"&gt;RailsEventStore&lt;/a&gt; together with the &lt;a href="https://github.com/RailsEventStore/rails_event_store/tree/master/contrib/ruby_event_store-process_manager"&gt;ruby_event_store-process_manager&lt;/a&gt; gem, allows to map complex business processes into modules, which encapsulates the logic, but hides the infrastructure.&lt;/p&gt;

&lt;p&gt;In a typical non-event-driven Rails app such processes are usually scattered across models, service objects and callbacks. The alternative approach requires publishing events (which provides event log for free) but the benefits might be worth it.&lt;/p&gt;
</content>
  </entry>
</feed>

