{"id":16818357,"url":"https://github.com/igr/ether","last_synced_at":"2026-01-20T14:34:22.300Z","repository":{"id":231513592,"uuid":"781935068","full_name":"igr/ether","owner":"igr","description":"Event Driven Something","archived":false,"fork":false,"pushed_at":"2024-04-08T12:47:39.000Z","size":479,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-07T07:34:04.218Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"igr"}},"created_at":"2024-04-04T10:20:26.000Z","updated_at":"2024-08-18T06:11:45.000Z","dependencies_parsed_at":"2025-02-13T11:42:42.892Z","dependency_job_id":null,"html_url":"https://github.com/igr/ether","commit_stats":null,"previous_names":["igr/tudu"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/igr/ether","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igr%2Fether","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igr%2Fether/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igr%2Fether/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igr%2Fether/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igr","download_url":"https://codeload.github.com/igr/ether/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igr%2Fether/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28604963,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T12:01:53.233Z","status":"ssl_error","status_checked_at":"2026-01-20T12:01:46.545Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T10:49:53.216Z","updated_at":"2026-01-20T14:34:22.282Z","avatar_url":"https://github.com/igr.png","language":"Kotlin","funding_links":["https://github.com/sponsors/igr"],"categories":[],"sub_categories":[],"readme":"# **`Ether`** ♒️ \u0026 **`Matter`** ⚛️\n\nWelcome to Event-Driven thought experiment ended up as a blueprint for a small engine.\n\n⚠️ This is `Either\u003cStupid, Great\u003e`, still can't figure. Built in 3 days time.\n\nFrankly, there is nothing new here; but I didn't see this exact combination of ideas in the wild. 🤷‍♂️\n\nThe premise:\n\n\u003e We can build distributed, scalable event-driven app with only **4** abstractions: `Pipe`, `Event`, `Realm`, `Ether`. If we add fifth: `Matter`, we can achieve a pure business logic.\n\nAnd:\n\n\u003e Whether the app is a simple CRUD or a complex event-sourced system, the abstractions should stay the same. The business logic should be pure, event-management and the state handling should be extracted\n\nEvery app is an event-driven app. We should focus on the business functionalities. The actual state handling and event management should be abstracted away. We should be able to _change_ the implementation of the infrastructure without changing the business logic _any time_. For example, you could switch from a traditional database to an event-sourced system without changing the business logic.\n\n## Pipes 🌊\n\n```\nPipe == Input -\u003e UnitOfWork -\u003e Output\n```\n\n⭐️ The **unit of work** (UOW) is just a function of a _single_ input, producing a _single_ output. We can connect UOWs by their input/output types, like... pipes. Hence we refer the UOW as a **pipe**.\n\n**Q**: Why single input/output? Bear with me. We can always aggregate any number of related objects into a single object, so it is not a limitation.\n\n⭐️ A **pipe** is a function that takes a single _input_ and produces a single _output_. It represents a **unit of work**. Due to singular input/outputs, pipes can be connected to each other, forming a mesh.\n\nWe may say that the application is a mesh of pipes, connected together. Here is a beautiful schema that illustrates the idea:\n\n![](./doc/mesh.png)\n\nBlue arrows represent pipes, connected to each other.\n\nExample of a pipe:\n\n```kt\nval createToDoList = Pipe\u003cToDoListCreateRequested\u003e {\n    val list = Store.createNewDraftToDoList(it.listId)\n    ToDoListCreated(list.id)\n}\n```\n\nThat's it, no surprises here.\n\n**Q**: What about errors/exceptions? There is no such thing as business exception. There is only a _fact_ that something happened - or failed to happen. The failure is just another resulting event.\n\n## Events ⚡️\n\n```\nEvent == Fact, Message, Input, Output\n```\n\n⭐️ `Event` has a multitude of meanings. It is a **fact** that something happened. It is a **message** passed between pipes. It is an **input** to the pipe. It is an **output** of the pipe. It connects pipes together.\n\n**Q**: Is a pipe actually a _event handler_? Possibly. Is a pipe a _command_? Possibly. I just like to think of it as a _unit of work_.\n\n⭐️ `Event` holds the necessary data for the pipe to do its work. It is a simple serializable data object.\n\n⭐️ `BlackHole` is a sink event. It is a special event, used to terminate the event flow. It is like a `null` in the event world.\n\nPipes that are updating _projections_ are the ones that usually returns the `BlackHole` event.\n\n## Realm 🌌\n\n⭐️ Events belong to a **Realm**. Realm is a simple name that represents a boundary.\n\n⭐️ Events are executed one after the another, in the single-threaded fashion, _within the same boundary_. This is important, as it allows us to have a consistent state of the application.\n\nHaving single-threaded pipe execution is a big deal, as it simplifies the state handling. We don't need to worry about the concurrent state changes. Realm allows parallel execution of the pipes in different realms.\n\n⭐️ Realm is distributed, i.e. realms may spread over the nodes. You may simply deploy realms on different nodes, allowing each node to handle a different realm (in single-threaded fashion).\n\n## Ether ♒\n\n```\nEther == Runner\n```\n\n⭐️ `Ether` is a glorious name for the Event bus engine abstraction that connects pipes and runs events on it. It is a very simple abstraction, that can be implemented in various ways. In this example, it is implemented with [NATS](https://nats.io/).\n\nEvent may be fired (and forget):\n\n```kt\nether.emit(ToDoListCreateRequested(id))\n```\n\nThis will trigger the execution of all pipes connected somehow to the initial event. The execution is asynchronous and non-blocking the calling point.\n\nEvent may be fired with a in-place listener:\n\n```kt\nether.emit(ToDoListSaveRequested(listId, name)) { event, finish -\u003e\n    if (event is ToDoListSaved) {\n        UIOperations.success(operationId, event)\n        finish()\n    } else if (event is ToDoListNotSaved) {\n        UIOperations.failure(operationId, event)\n        finish()\n    }\n}\n```\n\nCool thing here is that provided lambda ONLY listens to events in the context of the _current_ execution. It is NOT a global listener. Again, ONLY events that are created by pipes executed during this operation will be handled. This is cool when we want to have a listener that is only active during the current operation (non-blocking request/response)\n\n⭐️ Pipes are never invoked directly - there must be an event that triggers the execution. Only `Ether` executes pipes.\n\n⭐️ `Ether` is distributed! Pipes may be placed on different nodes:\n\n![](./doc/mesh-2.png)\n\nWhen the event `A` is fired, it will execute `foo` on node 1 and then `bar` on node 2.\n\n⭐️  Pipe also may be horizontally scaled (⚠️ not implemented in this example). That would mean that the `foo` pipe is executed on multiple nodes, but only one of them will handle the event. This happens out-of-box by simply deploying the same pipe on the multiple nodes.\n\n## Matter ⚛️\n\n```\nPipe = Pure + Matter\n```\n\nWe can optionally go further with abstractions and remove explicit state handling from the `Pipe` functions. This is where the `Matter` comes in. It is a simple interface that knows how to:\n\n+ load state from the storage for given (input) event\n+ save state to the storage for given (resulting) event\n\nThis allows us to extract state handling and have **pure functions** that are only concerned with the business logic.\n\n⭐️ `Matter` implementation is done by user:\n\n```kt\nclass StoreMatter : Matter {\n    override fun \u003cS\u003e loadState(event: Event): S {\n        return when (event) {\n            is ToDoListSaveRequested -\u003e {\n                SaveToDoListState(...)\n            }\n\t\t}\n    }\n\n    override fun \u003cSIN\u003e saveState(state: SIN, event: Event) {\n        when (event) {\n            is ToDoListSaved -\u003e {\n                // save state\n\t\t\t}\n        }\n    }\n}\n```\n\nNotice the `SaveToDoListState` - simple data class that holds the necessary input state for the business logic.\n\n⭐️ Our `Pipe` may be designed now as a `Pure`:\n\n```kt\nval saveToDoList = Pure\u003cToDoListSaveRequested, SaveToDoListState\u003e { _, it -\u003e\n    if (it.draftToDoList == null) {\n        return@Pure ToDoListNotSaved(\"Draft list not found\")\n    }\n    val newTodoList = ToDoList(it.draftToDoList.id, it.name)\n    ToDoListSaved(newTodoList)\n}\n```\n\nIt is a pure function!\n\n**Q**: Is it a `decider`/`evolver` combined? Possibly.\n\n`Pure` function is transformed into a `Pipe` by the... well, `Piper`:\n\n```kt\nPiper(matter)(saveToDoList)\n```\n\n⭐️ `Matter` may be implemented in various ways:\n\n+ transactional, traditional database\n+ in-memory, for testing and local development\n+ event-sourced, for the event-sourced systems\n\n## Infrastructure ⚙️\n\n```\nInstrastructure == Implementation detail\n```\n\n⭐️ Infrastructure is an implementation detail.\n\n⭐️ [NATS](https://nats.io) cluster with JetStream - used as the _implementation_ of the `Ether` in the example. `Ether` itself has very simple interface (abstraction) that could be easily replaced with another event engine. Moreover, we can have an in-memory implementation for local development and testing.\n\nWith Nats, pipes can be deployed anywhere. There is also an option that I didn't have time to explore for horizontal scaling of the pipes (using Nats groups).\n\n⚠️ I haven't spent much time on the infrastructure part, so it is a bit rough, maybe not working as expected.\n\n⭐️ [VertX](https://vertx.io/) for the API layer - because of its async nature, VertX seem as an excellent choice for the API layer.\n\n\n## Example 🎉\n\nThis very simple example illustrates the idea.\n\n+ REST endpoint that triggers the creation of the ToDo list (async)\n+ Operation tracker that returns the status of the operation (async)\n+ Connected pipes\n+ Update of the projection\n+ In-Place handler\n+ Distributed pipes\n\nThe storage atm is just a simple in-memory map.\n\nCheck out the `http` folder.\n\n## Should I stay or should I go? 🚶‍♂️‍➡️\n\nI _feel_ potential in presented concepts and this engine, but I am just tired and can not think straight 🤷‍♂️ **Let me know.**\n\nTODO:\n\n+ [ ] Horizontal scaling of the pipes using Nats groups\n+ [ ] Add Postgres example\n+ [ ] Add Event Sourcing example\n\nFinally:\n\n+ If this make sense, I would like to thank: [Dejan](https://github.com/DejanMilicic), [Ivan](https://fraktalio.com). They know way more than me about this stuff.\n+ If this is stupid, that's on me only :)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figr%2Fether","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figr%2Fether","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figr%2Fether/lists"}