{"id":25816399,"url":"https://github.com/kozmod/oniontx","last_synced_at":"2026-02-20T10:01:05.738Z","repository":{"id":167360417,"uuid":"642975631","full_name":"kozmod/oniontx","owner":"kozmod","description":"The library for transferring transaction management to the application layer.","archived":false,"fork":false,"pushed_at":"2026-02-18T13:42:00.000Z","size":836,"stargazers_count":6,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-02-18T16:45:44.819Z","etag":null,"topics":["clean-architecture","delivery-layer","go","golang","gorm-transaction","hexagonal-architecture","layer-architecture","mongo-transaction","onion-architecture","pgx-transaction","redis-transaction","repository","sql","sql-transaction","sql-transactions","sqlx-transaction","transaction","tx"],"latest_commit_sha":null,"homepage":"","language":"Go","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/kozmod.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":"CONTRIBUTING.md","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-05-19T19:30:42.000Z","updated_at":"2026-02-18T12:15:11.000Z","dependencies_parsed_at":null,"dependency_job_id":"5052caa1-bb28-45ae-8c26-d4a9a4400604","html_url":"https://github.com/kozmod/oniontx","commit_stats":null,"previous_names":["kozmod/oniontx"],"tags_count":88,"template":false,"template_full_name":null,"purl":"pkg:github/kozmod/oniontx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kozmod%2Foniontx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kozmod%2Foniontx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kozmod%2Foniontx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kozmod%2Foniontx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kozmod","download_url":"https://codeload.github.com/kozmod/oniontx/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kozmod%2Foniontx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29647768,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T09:27:29.698Z","status":"ssl_error","status_checked_at":"2026-02-20T09:26:12.373Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: 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":["clean-architecture","delivery-layer","go","golang","gorm-transaction","hexagonal-architecture","layer-architecture","mongo-transaction","onion-architecture","pgx-transaction","redis-transaction","repository","sql","sql-transaction","sql-transactions","sqlx-transaction","transaction","tx"],"created_at":"2025-02-28T05:21:20.738Z","updated_at":"2026-02-20T10:01:05.731Z","avatar_url":"https://github.com/kozmod.png","language":"Go","readme":"# oniontx \u003cimg align=\"right\" src=\".github/assets/onion_1.png\" alt=\"drawing\"  width=\"90\" /\u003e\n[![test](https://github.com/kozmod/oniontx/actions/workflows/test.yml/badge.svg)](https://github.com/kozmod/oniontx/actions/workflows/test.yml)\n[![Release](https://github.com/kozmod/oniontx/actions/workflows/release.yml/badge.svg)](https://github.com/kozmod/oniontx/actions/workflows/release.yml)\n![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/kozmod/oniontx)\n[![Go Report Card](https://goreportcard.com/badge/github.com/kozmod/oniontx)](https://goreportcard.com/report/github.com/kozmod/oniontx)\n![GitHub release date](https://img.shields.io/github/release-date/kozmod/oniontx)\n![GitHub last commit](https://img.shields.io/github/last-commit/kozmod/oniontx)\n[![GitHub MIT license](https://img.shields.io/github/license/kozmod/oniontx)](https://github.com/kozmod/oniontx/blob/dev/LICENSE)\n\n`oniontx` enables moving transaction management from the `Persistence` (repository) layer \nto the `Application` (service) (service) layer using an owner-defined contract.\n\n## Transactor\n\n# \u003cimg src=\".github/assets/clean_arch+uml.png\" alt=\"drawing\"  width=\"700\" /\u003e\n🔴 **NOTE:** `Transactor` was designed to work with only a single instance of a \"repository\" (`*sql.DB`, etc.).\nFor multiple repositories, use `Transactor` with `Sage`[\u003csup\u003e**ⓘ**\u003c/sup\u003e](#saga).\n\n### The key features:\n - [**simple implementation for `stdlib`**](#libs)\n - [**simple implementation for popular libraries**](#libs)\n - [**custom implementation's contract**](#custom)\n - [**simple testing with testing frameworks**](#testing)\n\n---\n### \u003ca name=\"libs\"\u003e\u003ca/\u003eDefault implementation examples for libs\n[test/integration](https://github.com/kozmod/oniontx/tree/master/test) module contains  examples \nof default `Transactor` implementations (stdlib, sqlx, pgx, gorm, redis, mongo):\n- [stdlib](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/stdlib)\n- [sqlx](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/sqlx)\n- [pgx](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/pgx)\n- [gorm](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/gorm)\n- [redis](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/redis)\n- [mongo](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/mongo)\n\n---\n\n##  \u003ca name=\"custom\"\u003e\u003ca/\u003eCustom implementation\nIf required, `oniontx` provides the ability to \nimplement custom algorithms for managing transactions (see examples).\n#### Interfaces:\n```go \ntype (\n\t// Mandatory\n\tTxBeginner[T Tx] interface {\n\t\tcomparable\n\t\tBeginTx(ctx context.Context) (T, error)\n\t}\n\t\n\t// Mandatory\n\tTx interface {\n\t\tRollback(ctx context.Context) error\n\t\tCommit(ctx context.Context) error\n\t}\n\n\t// Optional - using to putting/getting transaction from `context.Context` \n\t// (library contains default `СtxOperator` implementation)\n\tСtxOperator[T Tx] interface {\n\t\tInject(ctx context.Context, tx T) context.Context\n\t\tExtract(ctx context.Context) (T, bool)\n\t}\n)\n```\n### Examples \n`❗` ️***These examples are based on the `stdlib` package.***\n\n`TxBeginner` and `Tx` implementations:\n```go\n// Prepared contracts for execution\npackage db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/kozmod/oniontx/mtx\"\n)\n\n// Executor represents common methods of sql.DB and sql.Tx.\ntype Executor interface {\n\tExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)\n}\n\n// DB is sql.DB wrapper, implements mtx.TxBeginner.\ntype DB struct {\n\t*sql.DB\n}\n\nfunc (db *DB) BeginTx(ctx context.Context) (*Tx, error) {\n\tvar txOptions sql.TxOptions\n\tfor _, opt := range opts {\n\t\topt.Apply(\u0026txOptions)\n\t}\n\ttx, err := db.DB.BeginTx(ctx, \u0026txOptions)\n\treturn \u0026Tx{Tx: tx}, err\n}\n\n// Tx is sql.Tx wrapper, implements mtx.Tx.\ntype Tx struct {\n\t*sql.Tx\n}\n\nfunc (t *Tx) Rollback(_ context.Context) error {\n\treturn t.Tx.Rollback()\n}\n\nfunc (t *Tx) Commit(_ context.Context) error {\n\treturn t.Tx.Commit()\n}\n```\n`Repositories` implementation:\n```go\npackage repoA\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/kozmod/oniontx/mtx\"\n\n\t\"github.com/user/some_project/internal/db\"\n)\n\ntype RepositoryA struct {\n\tTransactor *mtx.Transactor[*db.DB, *db.Tx]\n}\n\nfunc (r RepositoryA) Insert(ctx context.Context, val int) error {\n\tvar executor db.Executor\n\texecutor, ok  := r.Transactor.TryGetTx(ctx)\n\tif !ok {\n\t\texecutor = r.Transactor.TxBeginner()\n\t}\n\t_, err := executor.ExecContext(ctx, \"UPDATE some_A SET value = $1\", val)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"update 'some_A': %w\", err)\n\t}\n\treturn nil\n}\n```\n```go\npackage repoB\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\n\t\"github.com/kozmod/oniontx/mtx\"\n\t\n\t\"github.com/user/some_project/internal/db\"\n)\n\ntype RepositoryB struct {\n\tTransactor *mtx.Transactor[*db.DB, *db.Tx]\n}\n\nfunc (r RepositoryB) Insert(ctx context.Context, val int) error {\n\tvar executor db.Executor\n\texecutor, ok := r.Transactor.TryGetTx(ctx)\n\tif !ok {\n\t\texecutor = r.Transactor.TxBeginner()\n\t}\n\t_, err := executor.ExecContext(ctx, \"UPDATE some_A SET value = $1\", val)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"update 'some_A': %w\", err)\n\t}\n\treturn nil\n}\n```\n`UseCase` implementation:\n```go\npackage usecase\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\ntype (\n\t// transactor is the contract of  the mtx.Transactor\n\ttransactor interface {\n\t\tWithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)\n\t}\n\n\t// Repo is the contract of repositories\n\trepo interface {\n\t\tInsert(ctx context.Context, val int) error\n\t}\n)\n\ntype UseCase struct {\n\tRepoA repo\n\tRepoB repo\n\n\tTransactor transactor\n}\n\nfunc (s *UseCase) Exec(ctx context.Context, insert int) error {\n\terr := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {\n\t\tif err := s.RepoA.Insert(ctx, insert); err != nil {\n\t\t\treturn fmt.Errorf(\"call repository A: %w\", err)\n\t\t}\n\t\tif err := s.RepoB.Insert(ctx, insert); err != nil {\n\t\t\treturn fmt.Errorf(\"call repository B: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\" execute: %w\", err)\n\t}\n\treturn nil\n}\n```\nConfiguring:\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"os\"\n\n\t\"github.com/kozmod/oniontx/mtx\"\n\t\n\t\"github.com/user/some_project/internal/repoA\"\n\t\"github.com/user/some_project/internal/repoB\"\n\t\"github.com/user/some_project/internal/usecase\"\n)\n\n\nfunc main() {\n\tvar (\n\t\tdatabase *sql.DB // database pointer\n\n\t\twrapper    = \u0026db.DB{DB: database}\n\t\toperator   = mtx.NewContextOperator[*db.DB, *db.Tx](\u0026wrapper)\n\t\ttransactor = mtx.NewTransactor[*db.DB, *db.Tx](wrapper, operator)\n\n\t\trepositoryA = repoA.RepositoryA{\n\t\t\tTransactor: transactor,\n\t\t}\n\t\trepositoryB = repoB.RepositoryB{\n\t\t\tTransactor: transactor,\n\t\t}\n\n\t\tuseCase = usecase.UseCase{\n\t\t\tRepoA: \u0026repositoryA,\n\t\t\tRepoB: \u0026repositoryB,\n\t\t\tTransactor:  transactor,\n\t\t}\n\t)\n\n\terr := useCase.Exec(context.Background(), 1)\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n```\n---\n#### Execution transaction in the different use cases\n***Executing the same transaction for different `UseCases` using the same `Transactor` instance***\n\nUseCases:\n```go\npackage a\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\ntype (\n\t// transactor is the contract of  the mtx.Transactor\n\ttransactor interface {\n\t\tWithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)\n\t}\n\n\t// Repo is the contract of repositories\n\trepoA interface {\n\t\tInsert(ctx context.Context, val int) error\n\t\tDelete(ctx context.Context, val float64) error\n\t}\n)\n\ntype UseCaseA struct {\n\tRepo repoA\n\n\tTransactor transactor\n}\n\nfunc (s *UseCaseA) Exec(ctx context.Context, insert int, delete float64) error {\n\terr := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {\n\t\tif err := s.Repo.Insert(ctx, insert); err != nil {\n\t\t\treturn fmt.Errorf(\"call repository - insert: %w\", err)\n\t\t}\n\t\tif err := s.Repo.Delete(ctx, delete); err != nil {\n\t\t\treturn fmt.Errorf(\"call repository - delete: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"usecaseA - execute: %w\", err)\n\t}\n\treturn nil\n}\n```\n```go\npackage b\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\ntype (\n\t// transactor is the contract of  the mtx.Transactor\n\ttransactor interface {\n\t\tWithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)\n\t}\n\n\t// Repo is the contract of repositories\n\trepoB interface {\n\t\tInsert(ctx context.Context, val string) error\n\t}\n\n\t// Repo is the contract of the useCase\n\tuseCaseA interface {\n\t\tExec(ctx context.Context, insert int, delete float64) error\n\t}\n)\n\ntype UseCaseB struct {\n\tRepo     repoB\n\tUseCaseA useCaseA\n\n\tTransactor transactor\n}\n\nfunc (s *UseCaseB) Exec(ctx context.Context, insertA string, insertB int, delete float64) error {\n\terr := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {\n\t\tif err := s.Repo.Insert(ctx, insertA); err != nil {\n\t\t\treturn fmt.Errorf(\"call repository - insert: %w\", err)\n\t\t}\n\t\tif err := s.UseCaseA.Exec(ctx, insertB, delete); err != nil {\n\t\t\treturn fmt.Errorf(\"call usecaseB - exec: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"execute: %w\", err)\n\t}\n\treturn nil\n}\n```\nMain:\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"os\"\n\n\t\"github.com/kozmod/oniontx/mtx\"\n\n\t\"github.com/user/some_project/internal/db\"\n\t\"github.com/user/some_project/internal/repoA\"\n\t\"github.com/user/some_project/internal/repoB\"\n\t\"github.com/user/some_project/internal/usecase/a\"\n\t\"github.com/user/some_project/internal/usecase/b\"\n)\n\nfunc main() {\n\tvar (\n\t\tdatabase *sql.DB // database pointer\n\n\t\twrapper    = \u0026db.DB{DB: database}\n\t\toperator   = mtx.NewContextOperator[*db.DB, *db.Tx](\u0026wrapper)\n\t\ttransactor = mtx.NewTransactor[*db.DB, *db.Tx](wrapper, operator)\n\n\t\tuseCaseA = a.UseCaseA{\n\t\t\tRepo: repoA.RepositoryA{\n\t\t\t\tTransactor: transactor,\n\t\t\t},\n\t\t}\n\n\t\tuseCaseB = b.UseCaseB{\n\t\t\tRepo: repoB.RepositoryB{\n\t\t\t\tTransactor: transactor,\n\t\t\t},\n\t\t\tUseCaseA: \u0026useCaseA,\n\t\t}\n\t)\n\n\terr := useCaseB.Exec(context.Background(), \"some_to_insert_useCase_A\", 1, 1.1)\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n```\n\n## \u003ca name=\"saga\"\u003e\u003ca/\u003eSaga\nThe implementation of the `Saga` pattern.\n\nExample:\n```go\nsteps := []Step{\n\t{\n\t\tName: \"first step\",\n\t\t// Action — a function to execute\n\t\tAction: func(ctx context.Context) error {\n\t\t\t// Action logic.\n\t\t\treturn nil\n\t\t},\n\t\t// Compensation — a function to compensate an action when an error occurs.\n\t\t//\n\t\t// Parameters:\n\t\t//   - ctx: context for cancellation and deadlines (context that is passed through the action)\n\t\t//   - aroseErr: error from the previous action that needs compensation\n\t\tCompensation: func(ctx context.Context, aroseErr error) error {\n\t\t\t// Action compensation logic.\n\t\t\treturn nil\n\t\t},\n\t\t// CompensationOnFail needs to add the current compensation to the list of compensations.\n\t\tCompensationOnFail: true,\n\t},\n}\n// Saga execution.\nerr := NewSaga(steps).Execute(context.Background())\nif err != nil {\n\t// Error handling.\n}\n```\n\n[More examples here](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/saga).\n\n\n## \u003ca name=\"testing\"\u003e\u003ca/\u003eTesting\n\n[test](https://github.com/kozmod/oniontx/tree/master/test) package contains useful examples for creating unit test:\n\n- [vektra/mockery **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/mockery)\n- [go.uber.org/mock/gomock **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/gomock)\n- [gojuno/minimock **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/minimock)","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkozmod%2Foniontx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkozmod%2Foniontx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkozmod%2Foniontx/lists"}