{"id":22260654,"url":"https://github.com/notxcain/aecor","last_synced_at":"2025-04-04T17:10:12.879Z","repository":{"id":8984106,"uuid":"60449930","full_name":"notxcain/aecor","owner":"notxcain","description":"Pure functional event sourcing runtime","archived":false,"fork":false,"pushed_at":"2024-08-13T00:41:25.000Z","size":20946,"stargazers_count":324,"open_issues_count":79,"forks_count":35,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-03-28T16:09:35.205Z","etag":null,"topics":["aecor","akka","behavior","cqrs","distributed","eventsourcing","functional-programming","pure","reactive","runtime","scala"],"latest_commit_sha":null,"homepage":"aecor.io","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/notxcain.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"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}},"created_at":"2016-06-05T08:13:57.000Z","updated_at":"2025-01-16T12:53:58.000Z","dependencies_parsed_at":"2024-01-15T08:53:50.415Z","dependency_job_id":"cd1a7db1-1cb1-47fd-b477-c739eb5e8683","html_url":"https://github.com/notxcain/aecor","commit_stats":{"total_commits":252,"total_committers":10,"mean_commits":25.2,"dds":0.3055555555555556,"last_synced_commit":"1293c6b01d5a102ea6d5d90726cb72ada8aba143"},"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notxcain%2Faecor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notxcain%2Faecor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notxcain%2Faecor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notxcain%2Faecor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/notxcain","download_url":"https://codeload.github.com/notxcain/aecor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247217222,"owners_count":20903009,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["aecor","akka","behavior","cqrs","distributed","eventsourcing","functional-programming","pure","reactive","runtime","scala"],"created_at":"2024-12-03T09:09:29.048Z","updated_at":"2025-04-04T17:10:12.863Z","avatar_url":"https://github.com/notxcain.png","language":"Scala","readme":"\n[![Build Status](https://img.shields.io/travis/notxcain/aecor/master.svg)](https://travis-ci.org/notxcain/aecor)\n[![Maven Central](https://img.shields.io/maven-central/v/io.aecor/core_2.12.svg)](https://search.maven.org/search?q=g:io.aecor%20AND%20a:core_2.12\u0026core=gav)\n[![Join the chat at https://gitter.im/notxcain/aecor](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/notxcain/aecor)\n[![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat\u0026logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)\n\n\n# Aecor\n### A pure functional library for defining and running eventsourced behaviors\n\nAecor is an opinionated library to help building scalable, distributed eventsourced services written in Scala.\n\nBuilt-in runtime implementation uses [Akka](https://github.com/akka/akka) for distribution and fault tolerance.\n\nIt heavily relies on [Cats](https://github.com/typelevel/cats) and [Cats Effect](https://github.com/typelevel/cats-effect)\n\nAecor works on Scala 2.12 with Java 8.\n\nThe name `Aecor` (_lat. ocean_) is inspired by a vision of modern distributed applications as an ocean of messages with pure behaviors floating in it.\n    \n### Installing Aecor\n\nTo start using Aecor Akka Persistence Runtime add the following to your `build.sbt` file:\n\n```scala\nscalaVersion := \"2.12.7\"\nscalacOptions += \"-Ypartial-unification\"\naddCompilerPlugin(\"org.scalameta\" % \"paradise\" % \"3.0.0-M11\" cross CrossVersion.full)\nlibraryDependencies += \"io.aecor\" %% \"akka-persistence-runtime\" % \"x.y.z\" // See current version on the badge above\n```\n\n### Media\n\n- Amazing [series](https://pavkin.ru/aecor-intro) on Aecor by [Vladimir Pavkin](https://github.com/vpavkin)\n\n### Entity Behavior Definition\n\nIn this short guide I'll show you how to define and deploy your first event sourced behavior on runtime backed by Akka Persistence and Akka Cluster Sharding.\n\nEach entity needs an identity, so let's start with identifier type:\n\n```scala\nfinal case class SubscriptionId(value: java.util.UUID) extends AnyVal\n```\n\nThen define what actions we're able to perform on `Subscription`\n\n```scala\nimport aecor.macros.boopickleWireProtocol\nimport cats.tagless.autoFunctorK\nimport boopickle.Default._\n\n@boopickleWireProtocol\n@autoFunctorK(false)\ntrait Subscription[F[_]] {\n  def createSubscription(userId: String, productId: String, planId: String): F[Unit]\n  def pauseSubscription: F[Unit]\n  def resumeSubscription: F[Unit]\n  def cancelSubscription: F[Unit]\n}\n```\n\nYou may notice that there is no `SubscriptionId` involved, and it's okay because this interface describes actions of a concrete `Subscription` and entity behavior should not know about its identity, it's behavior should be defined solely by its state.\n\nThere is an abstract type `F[_]` which stays for an effect (see [Rob Norris, Functional Programming with Effects](https://www.youtube.com/watch?v=po3wmq4S15A)) that would be performed during each action invocation.\n\nAlso being polymorphic in effect improves the reuse of this interface, you'll see it later.\n\n`@boopickleWireProtocol` - is a macro annotation that automates derivation of a `WireProtocol`, which is used by Akka Runtime to encode and decode actions and corresponding responses.\n\nWe are event sourced, so let's define our events:\n\n```scala\nimport aecor.runtime.akkapersistence.serialization._\n\nsealed abstract class SubscriptionEvent extends Product with Serializable\nobject SubscriptionEvent {\n  final case class SubscriptionCreated(userId: String, productId: String, planId: String) extends SubscriptionEvent\n  final case object SubscriptionPaused extends SubscriptionEvent\n  final case object SubscriptionResumed extends SubscriptionEvent\n  final case object SubscriptionCancelled extends SubscriptionEvent\n\n  implicit val persistentEncoder: PersistentEncoder[SubscriptionEvent] = ???\n  implicit val persistentDecoder: PersistentDecoder[SubscriptionEvent] = ???\n}\n```\n\nI've intentionally omitted implementation of `PersistentEncoder` and `PersistentDecoder`, because providing generic JSON encoding would be careless as persistent event schema requires your attention and I would recommend to use Protobuf or other formats that support schema evolution.\n\nLet's define a state on which `Subscription` operates.\n\n```scala\nimport aecor.data.Folded.syntax._\nimport SubscriptionState._\n\nfinal case class SubscriptionState(status: Status) {\n  def update(e: SubscriptionEvent): Folded[SubscriptionState] = e match {\n    case SubscriptionCreated(_, _, _) =\u003e\n      impossible\n    case SubscriptionPaused =\u003e\n      subscription.copy(status = Paused).next\n    case SubscriptionResumed =\u003e\n      subscription.copy(status = Active).next\n    case SubscriptionCancelled =\u003e\n      subscription.copy(status = Cancelled).next\n  }\n}\n\nobject SubscriptionState {\n  def create(e: SubscriptionEvent): Folded[SubscriptionState] = e match {\n    case SubscriptionCreated(userId, productId, planId) =\u003e\n      Subscription(Active).next\n    case _ =\u003e impossible\n  }\n  sealed abstract class Status extends Product with Serializable\n    object Status {\n      final case object Active extends Status\n      final case object Paused extends Status\n      final case object Cancelled extends Status\n    }\n}\n\n```\n\nPay attention to `Folded` datatype, it has to constructor:\n- `Impossible` is used to express impossible folds of events, so that you don't throw exceptions.\n- `Next(a: A)` is used to express successful event application.\n\n\nNow, the final part before we launch.'\n\nAs I said earlier `Subscription[F[_]]` is polymorphic in its effect type.\n\nOur effect would be any `F[_]` with instance of `MonadAction[F, Option[SubscriptionState], SubscriptionEvent]` which provides essential operations for eventsources command handler\n* `read: F[Option[SubscriptionState]]` - reads current state\n* `append(event: SubscriptionEvent, other: SubscriptionEvent*): F[Unit]` - append one or more events\nOther stuff like state recovery and event persistence is held by Akka Persistence Runtime.\n\nSo lets define `SubscriptionActions`\n\n```scala\nimport cats.implicits._\n\nfinal class SubscriptionActions[F[_]](\n  implicit F: MonadAction[F, Option[SubscriptionState], SubscriptionEvent]\n  ) extends Subscription[F] {\n\n  import F._ // import algebra functions\n\n  def createSubscription(userId: String, productId: String, planId: String): F[Unit] =\n    read.flatMap {\n      case Some(subscription) =\u003e\n        ignore\n      case None =\u003e\n        // Produce event\n        append(SubscriptionCreated(userId, productId, planId))\n    }\n\n  def pauseSubscription: F[Unit] =\n    read.flatMap {\n      case Some(subscription) if subscription.status == Active =\u003e\n        append(SubscriptionPaused)\n      case _ =\u003e\n        ignore\n    }\n\n  def resumeSubscription: F[Unit] =\n    read.flatMap {\n      case Some(subscription) if subscription.status == Paused =\u003e\n        append(SubscriptionResumed)\n      case _ =\u003e\n        ignore\n    }\n   \n  def cancelSubscription: F[Unit] =\n    read.flatMap {\n      case Some(subscription) if subscription.canCancel =\u003e\n        append(SubscriptionCancelled)\n      case _ =\u003e\n        ignore\n    }\n}\n```\n\nNow when actions are defined we're ready to deploy\n\n```scala\n\nimport cats.effect.IO\nimport aecor.runtime.akkapersistence._\n\nval system = ActorSystem(\"system\")\n\nval journalAdapter = CassandraJournalAdapter(system)\n\nval runtime = AkkaPersistenceRuntime(system, journalAdapter)\n\nval behavior: EventsourcedBehavior[Subscription, IO, Option[SubscriptionState], SubscriptionEvent]  \n  EventsourcedBehavior.optional(\n    new SubscriptionActions,\n    Fold.optional(SubscriptionState.create)(_.update(_))\n  )\n\nval deploySubscriptions: IO[SubscriptionId =\u003e Subscription[IO]] =\n  runtime.deploy(\n    \"Subscription\",\n    behavior,\n    Tagging.const[SubscriptionId](EventTag(\"Subscription\"))\n  )\n```\n\n# Projections\n\n```\nval journalQuery = runtime.journal\n```\n\n# Adopters\n\nUsing Aecor in your organization? Send us a PR to list your company here:\n\n+ [Evotor](https://evotor.ru/)\n","funding_links":[],"categories":["JVM"],"sub_categories":["Scala"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotxcain%2Faecor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnotxcain%2Faecor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotxcain%2Faecor/lists"}