{"id":21237354,"url":"https://github.com/mateuszkubuszok/useless","last_synced_at":"2025-07-10T18:31:49.486Z","repository":{"id":57726016,"uuid":"151277465","full_name":"MateuszKubuszok/useless","owner":"MateuszKubuszok","description":"Simple, dependency-free library for writing process managers.","archived":true,"fork":false,"pushed_at":"2020-10-30T15:28:06.000Z","size":114,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-26T05:44:21.199Z","etag":null,"topics":["dsl","process-manager","scala","transaction"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MateuszKubuszok.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}},"created_at":"2018-10-02T15:25:17.000Z","updated_at":"2024-08-27T11:12:43.000Z","dependencies_parsed_at":"2022-09-17T14:31:01.644Z","dependency_job_id":null,"html_url":"https://github.com/MateuszKubuszok/useless","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/MateuszKubuszok/useless","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MateuszKubuszok%2Fuseless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MateuszKubuszok%2Fuseless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MateuszKubuszok%2Fuseless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MateuszKubuszok%2Fuseless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MateuszKubuszok","download_url":"https://codeload.github.com/MateuszKubuszok/useless/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MateuszKubuszok%2Fuseless/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264631213,"owners_count":23640941,"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":["dsl","process-manager","scala","transaction"],"created_at":"2024-11-21T00:18:17.488Z","updated_at":"2025-07-10T18:31:49.145Z","avatar_url":"https://github.com/MateuszKubuszok.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Useless\n\n[![Build Status](https://travis-ci.org/MateuszKubuszok/useless.svg?branch=master)](https://travis-ci.org/MateuszKubuszok/useless)\n[![Maven Central](https://img.shields.io/maven-central/v/com.kubuszok/useless-core_2.12.svg)](https://search.maven.org/search?q=g:com.kubuszok%20useless)\n[![License](http://img.shields.io/:license-Apache%202-green.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt)\n\nSimple, dependency-free library for writing process managers.\n\n## Installation\n\nAdd to your `build.sbt`\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-core\" % uselessVersion\n```\n\nIf needed you might want to [install integration as well](#integrations).\n\n## Motivation\n\nSometimes you want to compose several services e.g.\n\n```scala\ndef createUser(userData: UserData): Future[User] = ...\ndef createUserResourceGroup(userID: User.ID): Future[ResourceGroup] = ...\ndef addEntitlementsToResourceGroup(\n    userID: User.ID,\n    resourceGroupID: ResourceGroup.ID\n): Future[Unit] = ...\n\ndef createAdmin(userData: UserData): Future[User] = for {\n  user \u003c- createUser(userData)\n  resourceGroup \u003c- createUserResourceGroup(user.id)\n  _ \u003c- addEntitlementsToResourceGroup(user.id, resourceGroup.id)\n} yield user\n```\n\nHowever, you soon find out that if server crashes, or if there is some\nconnection issue between (micro)services, this pipeline will fail and\nyou end up with half-made service call.\n\nServices lie in different contexts, so you cannot use database transaction\nto handle it. You might consider rewriting your API and conventions to\nCQRS and ES, but you might think, that what you actually want is an ability\nto define a simple saga-pattern like process manager. Possibly with a simple\nDSL.\n\nIt could look something like this:\n\n```scala\nval manger = Manager[Future]\n\nprivate val createAdminV1: UserData =\u003e Future[User] = manager(\"create-admin-v1\") {\n  ProcessBuilder\n    .create[Future, UserData]\n    .retryUntilSucceed(createUser)\n    .retryUntilSucceed(user =\u003e createUserResourceGroup(user).map(user.id -\u003e _.id))\n    .retryUntilSucceed(addEntitlementsToResourceGroup.tupled)\n}\n\ndef createAdmin(userData: UserData): Future[User] = createAdminV1(userData)\n```\n\nThat's the idea behind *useless* library.\n\n### Name\n\nA friend of mine told me this idea is retarded, as any perfectly written project\nwould not have such issues, and if you are in imperfect project then it's your\nproblem. So this project would be useless.\n\nI though that would be a perfect name for the project.\n\n## Goals\n\n * handle simple scenarios of cross-service transactions in cases where you\n   think full implementation of a saga-pattern would be an overkill,\n * supporting transactions between external services you have no control e.g.\n   in your backend app, which most of the time doesn't do complex things in\n   a sophisticated way,\n * helping projects where transactions between services are more\n   of exceptions than a rule, and so changes to whole architecture would be\n   hard to explain.\n\n## Non-goals\n\n * handling all possible cases with a support of all kinds of behaviors. This\n   library only wants to support retry or revert for each stage,\n * implementing saga-pattern and providing support and building blocks for\n   distributed transactions - if you have microservices communicating with\n   events use them to implement saga pattern instead.\n\n## Assumptions/contracts\n\nThe way idea (and assumptions) behind useless looks like this:\n\n * the service is split into stages - a stage is a function from some input `I`\n   to the output `F[O]` (you are able to configure whether `F` would be `Future`,\n   `Task`, `IO`, etc, basically TTFI),\n * at the beginning and end of each stage you persist the state (input/output)\n   to some persistent storage (`Journal`). It will allow restoring calls in case\n   JVM crashed etc,\n * we are assuming, that each stage is idempotent - it is something you, have\n   to take case of,\n * in order to persist the current state of the service input and output should\n   be (de)serializable - here we call it `PersistentArguments`,\n * implementation of `Journal` is also something you need to provide. This\n   way it will surely work out with how you persist things in your application,\n * to be able to resume interrupted services, `Manager` has to know about them.\n   So it is your responsibility to register all of them before calling\n   `manager.resumeInterruptedServices()`. (You don't need to use them all. You\n   register some services for a while to make sure they are finished, and then\n   remove them while only exposing the latest one),\n * out of the box, there are two strategies:\n   * retry until succeed - it has no assumption about reversibleness of each\n     stage. If service fail at stage with such recovering strategy , it will\n     try to rerun this stage until it succeed,\n   * revert - it is available only of all of the previous stages defined a\n     revert (rollback) function. On error it will revert _all previous stages_\n     to make it look as if transaction never occurred. (Of course revert\n     function should also be idempotent),\n * you are able to define your own strategy, that would make choice between\n   retry and revert at each stage, but that is experimental and underspecified.\n\n## Usage\n\nYou will setup things in following order:\n\n * creating `manager` that would handle the transactions for you,\n * passing `manager` to there your services are defined, so that they would\n   be both: defined and available to you and registered within manager,\n * once services are defined you can run `manager.resumeInterruptedServices()`\n   to make it use journal to resume all interrupted services.\n\nSee [example](example/src) to see how it can be used in a real app.\nEspecially [`AdminServices`](example/src/main/scala/useless/example/AdminServices.scala)\nand [`Example`](example/src/main/scala/useless/example/Example.scala).\n\n### Journal and Manager\n\nAt first, define a `journal` and `manager`:\n\n```scala\nimport useless._\n\nval journal: Journal[Future] = ??? // this doesn't have to be Future of course\nval manager: Manager[Future] = Manager[Future](journal)\n```\n\nBoth of these require an instance of `useless.algebra.MonadError[F, Throwable]`.\nManager, additionally `useless.algebra.Timer`.\nCurrently, only an instance for `Future` is defined, but there are extra modules\nfor lifting Cats/Scalaz instances for it (see below). (I didn't use any of them\nhere to make sure `useless-core` has literally no dependencies).\n\nNow, you can pass `manager` to where you define your services. If you want, you\nmight use type bounds to do it in a TTFI way.\n\n```scala\nimplicit val manager: Manager[F] = Manager[Future](journal)\n```\n\n```\nclass AdminServices[F[_]: Manager](...) {\n\n  val createAdminV1: UserData =\u003e Future[User] =\n    Manager[F].apply(\"create-admin-v1\") {\n      // ProcessBuilder definition here\n    }\n}\n```\n\nOnce all services are registered, you can resume interrupted ones with:\n\n```scala\nmanager.resumeInterruptedServices()\n```\n\nIt is your responsibility, to make sure there are no several instances of your\napplication, that would call this all at the same time.\n\n(Have I mentioned that, this aims to be simple? And that people call it useless\nfor a reason?)\n\n### Defining service\n\nFor all services, that should be transactional you have to register them using\n`manager`:\n\n```scala\nval createAdminV1: UserData =\u003e Future[User] = manager(\"create-admin-v1\") {\n  ProcessBuilder\n    .create[Future, UserData]\n    .retryUntilSucceed(createUser)\n    .retryUntilSucceed(user =\u003e createUserResourceGroup(user).map(user.id -\u003e _.id))\n    .retryUntilSucceed(addEntitlementsToResourceGroup.tupled)\n}\n```\n\nDefinition starts with a `ProcessBuilder.create[F, A]`. `F` is your IO type\n(`Future`, `IO`, `Task`, etc) matching the type of IO you choose for your\nJournal and Manager. `A` is a type of the argument passed to the service.\n\nYou are starting with a `ReversibleProcessBuilder` creating `A =\u003e F[A]`\nservice for which you will add building blocks that will take you from\n`A` to `F[B]`, from `B` to `F[C]` etc (like in monad, except monadic\ninterface had some troublesome issues, like how to handle service using 2\narguments from 2 previous stages? Now, you have to explicitly pass them\nthrough as a pair, so it's easy to reason, (de)serialize, resume, etc).\n\n```scala\n// this is virtually equal to (s: String) =\u003e Future.successful(s)\nval sth: String =\u003e Future[String] = manager(\"successful\") {\n  ProcessBuilder.create[Future, String]\n}\n```\n\n`Reversible` means you can roll it back. You can rollback as long as for\neach new stage you pair it with a revert function. You can choose rollback\nas a strategy of dealing with error as long as all previous stages were\nreversible.\n\n```scala\nProcessBuilder.create[Future, Int]\n  // defines revert -\u003e reversible stage\n  .retryUntilSuccess[String] { i =\u003e\n    Future(i.toString)\n  } { s =\u003e\n    Future(s.toInt)\n  }\n  // previous stage is revertible -\u003e can use revert\n  // doesn't define revert -\u003e is not revertible itself\n  .revertOnFirstFailure[String] { s =\u003e\n    Future(\"value is \" + s)\n  }\n  // previous chain is non-revertible\n  // this stage can only be non-revertible\n  .retryUntilSuccess[Unit] { s =\u003e\n    Future(println(s))\n  }\n```\n\nIn this example we have a definition for `Int =\u003e Future[Unit]`, that would:\n\n * would take an `Int` argument,\n * tried to turn it into `String` as many times as needed in order to succeed,\n * then tried to add a prefix to this String - on failure it would revert\n   whole transaction (which here means it would call `String =\u003e Future[Int]`),\n * finally, it would try to print the `String`. Again it would try to do it as\n   many times as needed to succeed.\n\nAt this point there are 4 possible strategies to deal with an error:\n\n * always retry,\n * revert everything on first error,\n * revert bounded with max attempts and with exponentially increasing delay\n * custom using `Input =\u003e F[(Input, Retry|Revert)]` function to decide\n\nAs a matter of the fact, bounded retry was implemented using custom strategy,\nwhile retry/revert are building blocks for more advanced strategies. Take\na look at [`useless.extra.BoundedRetry`](modules/core/src/main/scala/useless/extra/BoundedRetry.scala)\nto check implementation details.\n\n## Integrations\n\n### Cats\n\nAdd to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-cats\" % uselessVersion\n```\n\nthen import:\n\n```scala\nimport cats.implicits._\nimport useless.cats._\n```\n\nIt will allow you to convert `cats.MonadError`, `cats.Traverse`,\n`cats.effect.Timer` to `useless.algebra.MonadError`, `useless.algebra.Sequence`\nand `useless.algebra.Timer`.\n\n### Scalaz\n\nAdd to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-scalaz\" % uselessVersion\n```\n\nthen import:\n\n```scala\nimport scalaz._\nimport Scalaz._\nimport useless.scalaz._\n```\n\nIt will allow you to convert `scalaz.MonadError` and `scalaz.Traverse` to\n`useless.algebra.MonadError` and `useless.algebra.Sequence`.\n`useless.algebra.Timer` exist for `scalaz.ioeffect.Task`.\n\n### Doobie\n\nAdd to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-doobie\" % uselessVersion\n```\n\nthen import:\n\n```scala\nimport useless.doobie._\n```\n\nIt will allow you to convert create `Journal` with\n`new DoobieJournal(transactor)`. You can configure it using\n`DoobieJournal.Config`, as by default it expects table similar to:\n\n```sql\nCREATE TABLE journal (\n  service_name Text NOT NULL,\n  call_id      Text NOT NULL PRIMARY KEY,\n  stage_no     Int  NOT NULL,\n  argument     Text NOT NULL,\n  status       Text NOT NULL\n);\n```\n\n### Slick\n\nAdd to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-slick\" % uselessVersion\n```\n\nthen import:\n\n```scala\nimport useless.slick._\n```\n\nIt will allow you to convert create `Journal` with\n`new SlickJournal(database)`. You can configure it using\n`SlickJournal.Config`, as by default it expects table similar to:\n\n```sql\nCREATE TABLE journal (\n  service_name Text NOT NULL,\n  call_id      Text NOT NULL PRIMARY KEY,\n  stage_no     Int  NOT NULL,\n  argument     Text NOT NULL,\n  status       Text NOT NULL\n);\n```\n\n### Circe\n\nAdd to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-circe\" % uselessVersion\n```\n\nthen import:\n\n```scala\nimport useless.circe._\n```\n\nIt will allow you to convert `io.circe.Decoder` and `io.circe.Encoder` to\n`useless.PersistentArgument`.\n\n### PlayJSON\n\nAdd to `build.sbt`:\n\n```scala\nlibraryDependencies += \"com.kubuszok\" %% \"useless-play-json\" % uselessVersion\n```\n\nthen import:\n\n```scala\nimport useless.playjson._\n```\n\nIt will allow you to convert `play.api.libs.json.Reads` and\n`play.api.libs.json.Writes` to `useless.PersistentArgument`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmateuszkubuszok%2Fuseless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmateuszkubuszok%2Fuseless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmateuszkubuszok%2Fuseless/lists"}