{"id":27957011,"url":"https://github.com/gruelbox/transaction-outbox","last_synced_at":"2026-01-14T01:51:48.203Z","repository":{"id":37820620,"uuid":"247489164","full_name":"gruelbox/transaction-outbox","owner":"gruelbox","description":"Reliable eventual consistency for Microservices","archived":false,"fork":false,"pushed_at":"2025-12-05T10:02:22.000Z","size":2167,"stargazers_count":296,"open_issues_count":38,"forks_count":51,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-12-06T20:32:49.877Z","etag":null,"topics":["eventual-consistency","eventually-consistent","fifo-queue","kafka","kinesis","microservices","microservices-architecture","self-contained-system","transaction-manager"],"latest_commit_sha":null,"homepage":"","language":"Java","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/gruelbox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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":"2020-03-15T15:03:05.000Z","updated_at":"2025-12-03T15:47:59.000Z","dependencies_parsed_at":"2025-08-20T11:12:58.907Z","dependency_job_id":"ea880f74-0b6a-48fe-9b98-5471c2e603a1","html_url":"https://github.com/gruelbox/transaction-outbox","commit_stats":null,"previous_names":[],"tags_count":550,"template":false,"template_full_name":null,"purl":"pkg:github/gruelbox/transaction-outbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruelbox%2Ftransaction-outbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruelbox%2Ftransaction-outbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruelbox%2Ftransaction-outbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruelbox%2Ftransaction-outbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gruelbox","download_url":"https://codeload.github.com/gruelbox/transaction-outbox/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gruelbox%2Ftransaction-outbox/sbom","scorecard":{"id":446771,"data":{"date":"2025-08-11","repo":{"name":"github.com/gruelbox/transaction-outbox","commit":"7452c1100ea0a9790d026435bbea16f92d15983f"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":6.2,"checks":[{"name":"Code-Review","score":5,"reason":"Found 2/4 approved changesets -- score normalized to 5","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"29 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/cd_build.yml:1","Warn: no topLevel permission defined: .github/workflows/pull_request.yml:1","Warn: no topLevel permission defined: .github/workflows/release.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/cd_build.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/cd_build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/cd_build.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/cd_build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull_request.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/pull_request.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull_request.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/pull_request.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull_request.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/pull_request.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull_request.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/pull_request.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pull_request.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/pull_request.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/release.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/release.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:59: update your workflow using https://app.stepsecurity.io/secureworkflow/gruelbox/transaction-outbox/release.yml/master?enable=pin","Info:   0 out of   9 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/cd_build.yml:8"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"SAST","score":8,"reason":"SAST tool is not run on all commits -- score normalized to 8","details":["Warn: 24 commits out of 29 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T06:56:27.383Z","repository_id":37820620,"created_at":"2025-08-19T06:56:27.383Z","updated_at":"2025-08-19T06:56:27.383Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408692,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T00:40:43.272Z","status":"ssl_error","status_checked_at":"2026-01-14T00:40:42.636Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5: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":["eventual-consistency","eventually-consistent","fifo-queue","kafka","kinesis","microservices","microservices-architecture","self-contained-system","transaction-manager"],"created_at":"2025-05-07T18:02:20.945Z","updated_at":"2026-01-14T01:51:48.197Z","avatar_url":"https://github.com/gruelbox.png","language":"Java","funding_links":[],"categories":["数据库开发"],"sub_categories":[],"readme":"# transaction-outbox\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.gruelbox/transactionoutbox-core/badge.svg)](#stable-releases)\n[![Javadocs](https://www.javadoc.io/badge/com.gruelbox/transactionoutbox-core.svg?color=blue)](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core)\n[![GitHub Release Date](https://img.shields.io/github/release-date/gruelbox/transaction-outbox)](https://github.com/gruelbox/transaction-outbox/releases/latest)\n[![Latest snapshot](https://img.shields.io/github/v/tag/gruelbox/transaction-outbox?label=snapshot\u0026sort=semver)](#development-snapshots)\n[![GitHub last commit](https://img.shields.io/github/last-commit/gruelbox/transaction-outbox)](https://github.com/gruelbox/transaction-outbox/commits/master)\n[![CD](https://github.com/gruelbox/transaction-outbox/workflows/Continous%20Delivery/badge.svg)](https://github.com/gruelbox/transaction-outbox/actions)\n[![CodeFactor](https://www.codefactor.io/repository/github/gruelbox/transaction-outbox/badge)](https://www.codefactor.io/repository/github/gruelbox/transaction-outbox)\n\nA flexible implementation of the [Transaction Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html) for Java. `TransactionOutbox` has a clean, extensible API, very few dependencies and plays nicely with a variety of database platforms, transaction management approaches and application frameworks. Every aspect is highly configurable or overridable. It features out-of-the-box support for **Spring DI**, **Spring Txn**, **Guice**, **MySQL 5 \u0026 8**, **PostgreSQL 11-16**, **Oracle 18 \u0026 21**, **MS SQL Server 2017** and **H2**.\n\n## Contents\n\n1. [Why do I need it?](#why-do-i-need-it)\n1. [Installation](#installation)\n   1. [Requirements](#requirements)\n   1. [Stable releases](#stable-releases)\n   1. [Development snapshots](#development-snapshots)\n1. [Basic Configuration](#basic-configuration)\n   1. [No existing transaction manager or dependency injection](#no-existing-transaction-manager-or-dependency-injection)\n   1. [Spring](#spring)\n   1. [Guice](#guice)\n   1. [jOOQ](#jooq)\n1. [Set up the background worker](#set-up-the-background-worker)\n1. [Managing the \"dead letter queue\"](#managing-the-dead-letter-queue)\n1. [Advanced](#advanced)\n   1. [Topics and FIFO ordering](#topics-and-fifo-ordering)\n   1. [The nested outbox pattern](#the-nested-outbox-pattern)\n   1. [Idempotency protection](#idempotency-protection)\n   1. [Delayed/scheduled processing](#delayedscheduled-processing)\n   1. [Flexible serialization](#flexible-serialization-beta)\n   1. [Clustering](#clustering)\n   1. [OpenTelemetry](#opentelemetry)\n   1. [Encryption](#encryption)\n1. [Configuration reference](#configuration-reference)\n1. [Stubbing in tests](#stubbing-in-tests)\n\n## Why do I need it?\n\n[This article](https://microservices.io/patterns/data/transactional-outbox.html) explains the concept in an abstract manner, but let's say we have a microservice that handles point-of-sale and need to implement a REST endpoint to record a sale. We end up with this:\n\n### Attempt 1\n\n```java\n@POST\n@Path(\"/sales\")\n@Transactional\npublic SaleId createWidget(Sale sale) {\n  var saleId = saleRepository.save(sale);\n  messageQueue.postMessage(StockReductionEvent.of(sale.item(), sale.amount()));\n  messageQueue.postMessage(IncomeEvent.of(sale.value()));\n  return saleId;\n}\n```\n\nThe `SaleRepository` handles recording the sale in the customer's account, the `StockReductionEvent` goes off to our _warehouse_ service, and the `IncomeEvent` goes to our financial records service (let's ignore the potential flaws in the domain modelling for now).\n\nThere's a big problem here: the `@Transactional` annotation is a lie (no, [really](https://lmgtfy.com/?q=dont+use+distributed+transactions)). It only really wraps the `SaleRepository` call, but not the two event postings. This means that we could end up sending the two events and fail to actually commit the sale. Our system is now inconsistent.\n\n### Attempt 2 - Use Idempotency\n\nWe could make whole method [idempotent](http://restcookbook.com/HTTP%20Methods/idempotency/) and re-write it to work a bit more like this:\n\n```java\n@PUT\n@Path(\"/sales/{id}\")\npublic void createWidget(@PathParam(\"id\") SaleId saleId, Sale sale) {\n  saleRepository.saveInNewTransaction(saleId, sale);\n  messageQueue.postMessage(StockReductionEvent.of(saleId, sale.item(), sale.amount()));\n  messageQueue.postMessage(IncomeEvent.of(saleId, sale.value()));\n}\n```\n\nThis is better. As long as the caller keeps calling the method until they get a success, we can keep re-saving and re-sending the messages without any risk of duplicating work. This works regardless of the order of the calls (and in any case, there may be good reasons of referential integrity to fix the order).\n\nThe problem is that _they might stop trying_, and if they do, we could end up with only part of this transaction completed. If this is a public API, we can't force clients to use it correctly.\n\nWe also still have another problem: external calls are inherently more vulnerable to downtime and performance degredation. We could find our service rendered unresponsive or failing if they are unavailable. Ideally, we would like to \"buffer\" these external calls within our service safely until our downstream dependencies are available.\n\n### Attempt 3 - Transaction Outbox\n\nIdempotency is a good thing, so let's stick with the `PUT`. Here is the same example, using Transaction Outbox:\n\n```java\n@PUT\n@Path(\"/sales/{id}\")\n@Transactional\npublic void createWidget(@PathParam(\"id\") SaleId saleId, Sale sale) {\n  saleRepository.save(saleId, sale);\n  MessageQueue proxy = transactionOutbox.schedule(MessageQueue.class);\n  proxy.postMessage(StockReductionEvent.of(saleId, sale.item(), sale.amount()));\n  proxy.postMessage(IncomeEvent.of(saleId, sale.value()));\n}\n```\n\nHere's what happens:\n\n- When you create an instance of [`TransactionOutbox`](https://www.javadoc.io/static/com.gruelbox/transactionoutbox-core/0.1.57/com/gruelbox/transactionoutbox/TransactionOutbox.html) (see [Basic Configuration](#basic-configuration)), it will, by default, automatically create two database tables, `TXNO_OUTBOX` and `TXNO_VERSION`, and then keep these synchronized with schema changes as new versions are released. _Note: this is the default behaviour on a SQL database, but is completely overridable if you are using a different type of data store or don't want a third party library managing your database schema. See [Configuration reference](#configuration-reference)_. \n- [`TransactionOutbox`](https://www.javadoc.io/static/com.gruelbox/transactionoutbox-core/0.1.57/com/gruelbox/transactionoutbox/TransactionOutbox.html) creates a proxy of `MessageQueue`. Any method calls on the proxy are serialized and written to the `TXNO_OUTBOX` table (by default) _in the same transaction_ as the `SaleRepository` call. The call returns immediately rather than actually invoking the real method.\n- If the transaction rolls back, so do the serialized requests.\n- Immediately after the transaction is successfully committed, another thread will attempt to make the _real_ call to `MessageQueue` asynchronously.\n- If that call fails, or the application dies before the call is attempted, a [background \"mop-up\" thread](#set-up-the-background-worker) will re-attempt the call a configurable number of times, with configurable time between each, before [blocking](#managing-the-dead-letter-queue) the request and firing and event for it to be investigated (similar to a [dead letter queue](https://en.wikipedia.org/wiki/Dead_letter_queue)).\n- Blocked requests can be easily [unblocked](#managing-the-dead-letter-queue) again once the underlying issue is resolved.\n\nOur service is now resilient and explicitly eventually consistent, as long as all three elements (`SaleRepository` and the downstream event handlers) are idempotent, since those messages will be attempted repeatedly until confirmed successful, which means they could occur multiple times.\n\nIf you find yourself wondering _why bother with the queues now_? You're quite right. As we now have outgoing buffers, we already have most of the benefits of middleware (at least for some use cases). We could replace the calls to a message queue with direct queues to the other services' load balancers and switch to a peer-to-peer architecture, if we so choose.\n\n\u003e Note that for the above example to work, `StockReductionEvent` and `IncomeEvent` need to be included for serialization. See [Configuration reference](#configuration-reference).\n\n## Installation\n\n### Requirements\n- At least **Java 11**. Downgrading to requiring Java 8 is [under consideration](https://github.com/gruelbox/transaction-outbox/issues/29).\n- Currently, **MySQL**, **PostgreSQL**, **Oracle**, **MS SQL Server** or **H2** databases (pull requests to support other traditional RDMBS would be trivial. Beyond that, a SQL database is not strictly necessary for the pattern to work, merely a data store with the concept of a transaction spanning multiple mutation operations).\n- Database access via **JDBC** (In principle, JDBC should not be required - alternatives such as R2DBC are under investigation - but the API is currently tied to it)\n- Native transactions (not JTA or similar).\n- (Optional) Proxying non-interfaces requires [ByteBuddy](https://bytebuddy.net/#/) and for proxying classes without default constructors [Objenesis](http://objenesis.org/) to be added as a dependency\n\n### Stable releases\nThe latest stable release is available from Maven Central. Stable releases are [sort-of semantically versioned](https://semver.org/). That is, they follow semver in every respect other than that the version numbers are not monotically increasing. The project uses continuous delivery and selects individual stable releases to promote to Central, so Central releases will always be spaced apart numerically. The important thing, though, is that dependencies should be safe to upgrade as long as the major version number has not increased. \n\n#### Maven\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.gruelbox\u003c/groupId\u003e\n  \u003cartifactId\u003etransactionoutbox-core\u003c/artifactId\u003e\n  \u003cversion\u003e6.1.653\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n#### Gradle\n\n```groovy\nimplementation 'com.gruelbox:transactionoutbox-core:6.1.653'\n```\n\n### Development snapshots\n\nMaven Central is updated regularly. However, if you want to stay at the bleeding edge, you can use continuously-delivered releases from [Github Package Repository](https://github.com/gruelbox/transaction-outbox/packages). These can be used from your production builds since they will never be deleted (unlike `SNAPSHOT`s).\n\n#### Maven\n\n```xml\n\u003crepositories\u003e\n  \u003crepository\u003e\n    \u003cid\u003egithub-transaction-outbox\u003c/id\u003e\n    \u003cname\u003eGruelbox Github Repository\u003c/name\u003e\n    \u003curl\u003ehttps://maven.pkg.github.com/gruelbox/transaction-outbox\u003c/url\u003e\n  \u003c/repository\u003e\n\u003c/repositories\u003e\n```\n\nYou will need to authenticate with Github to use Github Package Repository. Create a personal access token in [your GitHub settings](https://github.com/settings/tokens). It only needs **read:package** permissions. Then add something like the following in your Maven `settings.xml`:\n\n```xml\n\u003cservers\u003e\n    \u003cserver\u003e\n        \u003cid\u003egithub-transaction-outbox\u003c/id\u003e\n        \u003cusername\u003e${env.GITHUB_USERNAME}\u003c/username\u003e\n        \u003cpassword\u003e${env.GITHUB_TOKEN}\u003c/password\u003e\n    \u003c/server\u003e\n\u003c/servers\u003e\n```\n\nThe above example uses environment variables, allowing you to keep the credentials out of source control, but you can hard-code them if you know what you're doing.\n\n#### Gradle\n\n```groovy\nrepositories {\n    maven {\n        name = \"github-transaction-outbox\"\n        url = uri(\"https://maven.pkg.github.com/gruelbox/transaction-outboxY\")\n        credentials {\n            username = $githubUserName\n            password = $githubToken\n        }\n    }\n}\n```\n\n## Basic Configuration\n\nAn application needs a single, shared instance of [`TransactionOutbox`](https://www.javadoc.io/static/com.gruelbox/transactionoutbox-core/0.1.57/com/gruelbox/transactionoutbox/TransactionOutbox.html), which is configured using a builder on construction. This takes some time to get right, particularly if you already have a transaction management solution in your application.\n\n### No existing transaction manager or dependency injection\n\nIf you have no existing transaction management, connection pooling or dependency injection, here's a quick way to get started:\n\n```java\n// Use an in-memory H2 database\nTransactionManager transactionManager = TransactionManager.fromConnectionDetails(\n    \"org.h2.Driver\", \"jdbc:h2:mem:test;MV_STORE=TRUE\", \"test\", \"test\"));\n\n// Create the outbox\nTransactionOutbox outbox = TransactionOutbox.builder()\n  .transactionManager(transactionManager)\n  .persistor(Persistor.forDialect(Dialect.H2))\n  .build();\n\n// Start a transaction\ntransactionManager.inTransaction(tx -\u003e {\n  // Save some stuff\n  tx.connection().createStatement().execute(\"INSERT INTO...\");\n  // Create an outbox request\n  outbox.schedule(MyClass.class).myMethod(\"Foo\", \"Bar\"));\n});\n```\n\nAlternatively, you could create the [`TransactionManager`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionManager.html) from a [`DataSource`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionManager.html), allowing you to use a connection pooling `DataSource` such as Hikari:\n\n```java\nTransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);\n```\n\nIn this default configuration, `MyClass` must have a default constructor so the \"real\" implementation can be constructed at the point the method is actually invoked (which might be on another day on another instance of the application). However, you can avoid this requirement by providing an [`Instantiator`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/Instantiator.html) on every instance of your application that knows how to create the objects:\n\n```java\nTransactionOutbox outbox = TransactionOutbox.builder()\n  .instantiator(Instantiator.using(clazz -\u003e createInstanceOf(clazz)))\n  .build();\n```\n\n### Spring\n\nSee [transaction-outbox-spring](transactionoutbox-spring/README.md), which integrates Spring's DI and/or transaction management with `TransactionOutbox`.\n\n### Guice\n\nSee [transaction-outbox-guice](transactionoutbox-guice/README.md), which integrates Guice DI `TransactionOutbox`.\n\n### jOOQ\n\nSee [transaction-outbox-jooq](transactionoutbox-jooq/README.md), which integrates jOOQ transaction management with `TransactionOutbox`.\n\n### Oracle\n\nOracle database compatibility requires to configure Oracle jdbc driver using following VM argument : -Doracle.jdbc.javaNetNio=false\n\n## Set up the background worker\n\nAt the moment, if any work fails first time, it won't be retried. All we need to add is a background thread that repeatedly calls [`TransactionOutbox.flush()`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionOutbox.html) to pick up and reprocess stale work.\n\nHow you do this is up to you; it very much depends on how background processing works in your application (a reactive solution will be very different to one based on Guava `Service`, for example). However, here is a simple example:\n\n```java\nThread backgroundThread = new Thread(() -\u003e {\n  while (!Thread.interrupted()) {\n    try {\n      // Keep flushing work until there's nothing left to flush\n      while (outbox.flush()) {}\n    } catch (Exception e) {\n      log.error(\"Error flushing transaction outbox. Pausing\", e);\n    }\n    try {\n       // When we run out of work, pause for a minute before checking again\n       Thread.sleep(60_000);\n    } catch (InterruptedException e) {\n       break;\n    }\n  }\n});\n\n// Startup\nbackgroundThread.start();\n\n// Shut down\nbackgroundThread.interrupt();\nbackgroundThread.join();\n```\n\n`flush()` is designed to handle concurrent use on databases that support `SKIP LOCKED`, such as Postgres and MySQL 8+. Feel free to run this as often as you like (within reason, e.g. once a minute) on every instance of your application.  This can have the benefit of spreading work across multiple instances when the work backlog is extremely high, but is not as effective as a proper [clustering](#clustering) approach.\n\nHowever, multiple concurrent calls to `flush()` can cause lock timeout errors on databases without `SKIP LOCKED` support, such as MySQL 5.7.  This is harmless, but will cause a lot of log noise, so you may prefer to run on a single instance at a time to avoid this.\n\n## Managing the \"dead letter queue\"\n\nWork might be retried too many times and enter a blocked state. You should set up an alert to allow you to manage this when it occurs, resolve the issue and unblock the work, since the work not being complete will usually be a sign that your system is out of sync in some way.\n\n```java\nTransactionOutbox.builder()\n    ...\n    .listener(new TransactionOutboxListener() {\n        @Override\n        public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n           // Spring example\n           applicationEventPublisher.publishEvent(new TransactionOutboxBlockedEvent(entry.getId(), cause);\n        }\n    })\n    .build();\n```\n\nTo mark the work for reprocessing, just use [`TransactionOutbox.unblock()`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionOutbox.html). Its failure count will be marked back down to zero and it will get reprocessed on the next call to `flush()`:\n\n```java\ntransactionOutboxEntry.unblock(entryId);\n```\n\nOr if using a `TransactionManager` that relies on explicit context (such as a non-thread local [`JooqTransactionManager`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-jooq/latest/com/gruelbox/transactionoutbox/JooqTransactionManager.html)):\n\n```java\ntransactionOutboxEntry.unblock(entryId, context);\n```\n\nA good approach here is to use the [`TransactionOutboxListener`](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/TransactionOutboxListener.html) callback to post an [interactive Slack message](https://api.slack.com/legacy/interactive-messages) - this can operate as both the alert and the \"button\" allowing a support engineer to submit the work for reprocessing.\n\n## Advanced\n\n### Topics and FIFO ordering\n\nFor some applications, the order in which tasks are processed is important, such as when:\n\n - using the outbox to write to a FIFO queue, Kafka or AWS Kinesis topic; or\n - data replication, e.g. when feeding a data warehouse or distributed cache.\n\nIn these scenarios, the default behaviour is unsuitable. Tasks are usually processed in a highly parallel fashion.\nEven if the volume of tasks is low, if a task fails and is retried later, it can easily end up processing after\nsome later task even if that later task was processed hours or even days after the failing one.\n\nTo avoid problems associated with tasks being processed out-of-order, you can order the processing of your tasks\nwithin a named \"topic\":\n\n```java\noutbox.with().ordered(\"topic1\").schedule(Service.class).process(\"red\");\noutbox.with().ordered(\"topic2\").schedule(Service.class).process(\"green\");\noutbox.with().ordered(\"topic1\").schedule(Service.class).process(\"blue\");\noutbox.with().ordered(\"topic2\").schedule(Service.class).process(\"yellow\");\n```\n\nNo matter what happens:\n\n - `red` will always need to be processed (successfully) before `blue`;\n - `green` will always need to be processed (successfully) before `yellow`; but\n - `red` and `blue` can run in any sequence with respect to `green` and `yellow`.\n\nThis functionality was specifically designed to allow outboxed writing to Kafka topics. For maximum throughput\nwhen writing to Kafka, it is advised that you form your outbox topic name by combining the Kafka topic and partition,\nsince that is the boundary where ordering is required.\n\nThere are a number of things to consider before using this feature:\n\n - Tasks are not processed immediately when submitting, as normal, and are processed by \n   background flushing only. This means there will be an increased delay between the source transaction being\n   committed and the task being processed, depending on how your application calls `TransactionOutbox.flush()`.\n - If a task fails, no further requests will be processed _in that topic_ until\n   a subsequent retry allows the failing task to succeed, to preserve ordered\n   processing. This means it is possible for topics to become entirely frozen in the event\n   that a task fails repeatedly. For this reason, it is essential to use a \n   `TransactionOutboxListener` to watch for failing tasks and investigate quickly. Note\n   that other topics will be unaffected.\n - `TransactionOutboxBuilder.blockAfterAttempts` is ignored for all tasks that use this\n   option.\n - A single topic can only be processed in single-threaded fashion, but separate topics can be processed in\n   parallel. If your tasks use a small number of topics, scalability will be affected since the degree of \n   parallelism will be reduced.\n\n### The nested-outbox pattern\n\nIn practice, it can be extremely hard to guarantee that an entire unit of work is idempotent and thus suitable for retry. For example, the request might be to \"update a customer record\" with a new address, but this might record the change to an audit history table with a fresh UUID, the current date and time and so on, which in turn triggers external changes outside the transaction. The parent customer update request may be idempotent, but the downstream effects may not be.\n\nTo tackle this, `TransactionOutbox` supports a use case where outbox requests spawn further outbox requests, along with a layer of additional [idempotency protection](#idempotency-protection) for particularly diffcult cases. The nested pattern works as follows:\n\n- Modify the customer record: `outbox.schedule(CustomerService.class).update(newDetails)`\n- The `update` method spawns a new outbox request to process the downstream effect: `outbox.schedule(AuditService.class).audit(\"CUSTOMER_UPDATED\", UUID.randomUUID(), Instant.now(), newDetails.customerId())`\n\nNow, if any part of the top-level request throws, nothing occurs. If the top level request succeeds, an idempotent request to create the audit record will retry safely.\n\n### Idempotency protection\n\nA common use case for `TransactionOutbox` is to receive an incoming request (such as a message from a message queue), acknowledge it immediately and process it asynchronously, for example:\n\n```java\npublic class FooEventHandler implements SQSEventHandler\u003cThingHappenedEvent\u003e {\n\n  @Inject private TransactionOutbox outbox;\n\n  public void handle(ThingHappenedEvent event) {\n    outbox.schedule(FooService.class).handleEvent(event.getThingId());\n  }\n}\n```\n\nHowever, incoming transports, whether they be message queues or APIs, usually need to rely on idempotency in message handlers (for the same reason that outgoing requests from outbox also rely on idempotency). This means the above code could get called twice.\n\nAs long as `FooService.handleEvent()` is idempotent itself, this is harmless, but we can't always assume this. The incoming message might be a broadcast, with no knowledge of the behaviour of handlers and therefore no way of pre-generating any new record ids the handler might need and passing them in the message.\n\nTo protect against this, `TransactionOutbox` can automatically detect duplicate requests and reject them with `AlreadyScheduledException`. Records of requests are retained up to a configurable threshold (see below).\n\nTo use this, use the call pattern:\n\n```java\noutbox.with()\n  .uniqueRequestId(\"context-clientid\")\n  .schedule(Service.class)\n  .process(\"Foo\");\n```\n\nWhere `context-clientid` is a globally-unique identifier derived from the incoming request. Such ids are usually available from queue middleware as message ids, or if not you can require as part of the incoming API (possibly with a tenant prefix to ensure global uniqueness across tenants).\n\n### Delayed/scheduled processing ###\n\nTo delay execution of a task, use:\n\n```java\noutbox.with()\n  .delayForAtLeast(Duration.of(5, MINUTES))\n  .schedule(Service.class)\n  .process(\"Foo\");\n```\n\nThere are some caveats around how accurate timing is. See the JavaDoc on the `delayForAtLeast` method for more information.\n\nThis is particularly useful when combined with the [nested outbox pattern](#the-nested-outbox-pattern) for creating polling/repeated or recursive tasks to throttle prcessing.\n\n### Flexible serialization (beta)\n\nMost people will use the default persistor, `DefaultPersistor`, to persist tasks to a relational database. This uses `DefaultInvocationSerializer` by default, which in turn uses [GSON](https://github.com/google/gson) to serialize as JSON.  `DefaultInvocationSerializer` is extremely limited by design, with a small list of allowed classes in method arguments. \nYou can extend the list of support types by calling `serializableTypes` in its builder, but it will always be restricted to this global list. This is by design, to avoid building a [deserialization of untrusted data](https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data) vulnerability into the library.\n\nFurthermore, there is no support for the case where run-time and compile-time types differ, such as in polymorphic collections. The following will always fail with `DefaultInvocationSerializer`:\n```java\noutbox.schedule(Service.class).processList(List.of(1, \"2\", 3L));\n```\nHowever, if you completely trust your serialized data (for example, your developers don't have write access to your production database, and the access credentials are well guarded) then you may prefer to have 100% flexibility, with no need to declare the types used and the ability to use any combination of run-time and compile-time types.\n\nSee [transaction-outbox-jackson](transactionoutbox-jackson/README.md), which uses a specially-configured Jackson `ObjectMapper` to achieve this.\n\n### Clustering\n\nThe default mechanism for _running_ tasks (either immediately, or when they are picked up by background processing) is via a `java.concurrent.Executor`, which effectively does the following:\n```java\nexecutor.execute(() -\u003e outbox.processNow(transactionOutboxEntry));\n```\nThis offloads processing to a background thread _on the application instance_ on which the task was picked up. Under high load, this can mean thousands of tasks being picked up from the database queue and submitted at the same time on the same application instance, even if there are 20 instances of the application, effectively limiting the total rate of processing to what a single instance can handle.\n\nIf you want to instead push the work for processing by _any_ of your application instances, thus spreading the work around a cluster, there are multiple approaches, just some of which are listed below:\n\n* An HTTP endpoint on a load-balanced DNS with service discovery (such as a container orchestrator e.g. Kubernetes or Nomad)\n* A shared queue (AWS SQS, ActiveMQ, ZeroMQ)\n* A lower-level clustering/messaging toolkit such as [JGroups](http://www.jgroups.org/).\n\nAll of these can be implemented as follows:\n\nWhen defining the `TransactionOutbox`, replace `ExecutorSubmitter` with something which serializes a `TransactionOutboxEntry` and ships it to the remote queue/address. Here's what configuration might look for a `RestApiSubmitter` which ships the request to a load-balanced endpoint hosted on Nomad/Consul:\n```java\nTransactionOutbox outbox = TransactionOutbox.builder().submitter(restApiSubmitter)\n```\nIt is strongly advised that you use a local executor in-line, to ensure that if there are communications issues with your endpoint or queue, it doesn't fail the calling thread.  Here is an example using [Feign](https://github.com/OpenFeign/feign):\n```java\n@Slf4j\nclass RestApiSubmitter implements Submitter {\n\n  private final FeignResource feignResource;\n  private final ExecutorService localExecutor;\n  private final Provider\u003cTransactionOutbox\u003e outbox;\n\n  @Inject\n  RestApiExecutor(String endpointUrl, ExecutorService localExecutor, ObjectMapper objectMapper, Provider\u003cTransactionOutbox\u003e outbox) {\n    this.feignResource = Feign.builder()\n      .decoder(new JacksonDecoder(objectMapper))\n      .target(GitHub.class, \"https://api.github.com\");;\n    this.localExecutor = localExecutor;\n    this.outbox = outbox;\n  }\n\n  @Override\n  public void submit(TransactionOutboxEntry entry, Consumer\u003cTransactionOutboxEntry\u003e leIgnore) {\n    try {\n      localExecutor.execute(() -\u003e processRemotely(entry));\n      log.info(\"Queued {} to be sent for remote processing\", entry.description());\n    } catch (RejectedExecutionException e) {\n      log.info(\"Will queue {} for processing when local executor is available\", entry.description());\n    } catch (Exception e) {\n      log.warn(\"Failed to queue {} for execution at {}. It will be re-attempted later.\", entry.description(), url, e);\n    }\n  }\n\n  private void processRemotely(TransactionOutboxEntry entry) {\n    try {\n      feignResource.process(entry);\n      log.info(\"Submitted {} for remote processing at {}\", entry.description(), url);\n    } catch (Exception e) {\n      log.warn(\n        \"Failed to submit {} for remote processing at {}. It will be re-attempted later.\",\n        entry.description(),\n        url,\n        e\n      );\n    }\n  }\n  \n  public interface FeignResource {\n    @RequestLine(\"POST /outbox/process\")\n    void process(TransactionOutboxEntry entry);\n  }\n  \n}\n```\nThen listen on your communication mechanism for incoming serialized `TransactionOutboxEntry`s, and push them to a normal local `ExecutorSubmitter`.  Here's what a JAX-RS example might look like:\n```java\n@POST\n@Path(\"/outbox/process\")\nvoid processOutboxEntry(String entry) {\n  TransactionOutboxEntry entry = somethingWhichCanSerializeTransactionOutboxEntries.deserialize(entry);\n  Submitter submitter = ExecutorSubmitter.builder().executor(localExecutor).logLevelWorkQueueSaturation(Level.INFO).build();\n  submitter.submit(entry, outbox.get()::processNow);\n}\n```\nThis whole approach is complicated a little by the inherent difficulty in serializing and deserializing a `TransactionOutboxEntry`, which is extremely polymorphic in nature. A reference approach is provided by [transaction-outbox-jackson](transactionoutbox-jackson/README.md), which provides the features necessary to make a Jackson `ObjectMapper` able to handle the work.  With that on the classpath you can use an `ObjectMapper` as follows:\n```java\n// Add support for TransactionOutboxEntry to your normal application ObjectMapper\nyourNormalSharedObjectMapper.registerModule(new TransactionOutboxJacksonModule());\n\n// (Optional) support deep polymorphic requests - for this we need to copy the object\n// mapper so it doesn't break the way the rest of your application works\nObjectMapper objectMapper = yourNormalSharedObjectMapper.copy();\nobjectMapper.setDefaultTyping(TransactionOutboxJacksonModule.typeResolver());\n\n// Serialize\nString message = objectMapper.writeValueAsString(entry);\n\n// Deserialize\nTransactionOutboxEntry entry = objectMapper.readValue(message, TransactionOutboxEntry.class);\n```\nArmed with the above, happy clustering!\n\n### OpenTelemetry\n\nA common request is to propagate [OTEL](https://opentelemetry.io/) traces from the calling code to an outboxed task. This can be achieved using `TransactionOutboxListener` to record the details of the parent `Span` as session variables, then restore them on invocation. Example:\n```java\nstatic class OtelListener implements TransactionOutboxListener {\n\n   /** Serialises the current context into {@link Invocation#getSession()}. */\n   @Override\n   public Map\u003cString, String\u003e extractSession() {\n      var result = new HashMap\u003cString, String\u003e();\n      SpanContext spanContext = Span.current().getSpanContext();\n      if (!spanContext.isValid()) {\n         return null;\n      }\n      result.put(\"traceId\", spanContext.getTraceId());\n      result.put(\"spanId\", spanContext.getSpanId());\n      log.info(\"Extracted: {}\", result);\n      return result;\n   }\n\n   /**\n    * Deserialises {@link Invocation#getSession()} and sets it as the current context so that any\n    * new span started by the method we invoke will treat it as the parent span\n    */\n   @Override\n   public void wrapInvocationAndInit(Invocator invocator) {\n      Invocation inv = invocator.getInvocation();\n      var spanBuilder =\n              otel.getTracer(\"transaction-outbox\")\n                      .spanBuilder(String.format(\"%s.%s\", inv.getClassName(), inv.getMethodName()))\n                      .setNoParent();\n      for (var i = 0; i \u003c inv.getArgs().length; i++) {\n         spanBuilder.setAttribute(\"arg\" + i, Utils.stringify(inv.getArgs()[i]));\n      }\n      if (inv.getSession() != null) {\n         var traceId = inv.getSession().get(\"traceId\");\n         var spanId = inv.getSession().get(\"spanId\");\n         if (traceId != null \u0026\u0026 spanId != null) {\n            spanBuilder.addLink(\n                    SpanContext.createFromRemoteParent(\n                            traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()));\n         }\n      }\n      var span = spanBuilder.startSpan();\n      try (Scope scope = span.makeCurrent()) {\n         invocator.runUnchecked();\n      } finally {\n         span.end();\n      }\n   }\n}\n```\nCheck out `AbstractAcceptanceTest.OtelListener` for an example of this in use.\n\n`extractSession` can be used for other things too, such as authentication state, HTTP session variables or anything else that might be in static context at the time of a call and you want to be inherited by the task.\n\n### Encryption\nBe warned that all data written to disk is written unencrypted by default (including MDC, session variables, method arguments...). If you may be writing sensitive data, particularly authentication-related data, you are advised to create your own `InvocationSerializer` by decorating an existing implementation, and use an efficient stream encryptor to write the data in encrupted form.\n\n## Configuration reference\n\nThis example shows a number of other configuration options in action:\n\n```java\nTransactionManager transactionManager = TransactionManager.fromDataSource(dataSource);\n\nTransactionOutbox outbox = TransactionOutbox.builder()\n    // The most complex part to set up for most will be synchronizing with your existing transaction\n    // management. Pre-rolled implementations are available for jOOQ and Spring (see above for more information)\n    // and you can use those examples to synchronize with anything else by defining your own TransactionManager.\n    // Or, if you have no formal transaction management at the moment, why not start, using transaction-outbox's\n    // built-in one?\n    .transactionManager(transactionManager)\n    // Modify how requests are persisted to the database. For more complex modifications, you may wish to subclass\n    // DefaultPersistor, or create a completely new Persistor implementation.\n    .persistor(DefaultPersistor.builder()\n        // Selecting the right SQL dialect ensures that features such as SKIP LOCKED are used correctly.\n        .dialect(Dialect.POSTGRESQL_9)\n        // Override the table name (defaults to \"TXNO_OUTBOX\")\n        .tableName(\"transactionOutbox\")\n        // Shorten the time we will wait for write locks (defaults to 2)\n        .writeLockTimeoutSeconds(1)\n        // Disable automatic creation and migration of the outbox table, forcing the application to manage\n        // migrations itself\n        .migrate(false)\n        // Allow the SaleType enum and Money class to be used in arguments (see example below)\n        .serializer(DefaultInvocationSerializer.builder()\n            .serializableTypes(Set.of(SaleType.class, Money.class))\n            .build())\n        .build())\n    // GuiceInstantiator and SpringInstantiator are great if you are using Guice or Spring DI, but what if you\n    // have your own service locator? Wire it in here. Fully-custom Instantiator implementations are easy to\n    // implement.\n    .instantiator(Instantiator.using(myServiceLocator::createInstance))\n    // Change the log level used when work cannot be submitted to a saturated queue to INFO level (the default\n    // is WARN, which you should probably consider a production incident). You can also change the Executor used\n    // for submitting work to a shared thread pool used by the rest of your application. Fully-custom Submitter\n    // implementations are also easy to implement, for example to cluster work.\n    .submitter(ExecutorSubmitter.builder()\n        .executor(ForkJoinPool.commonPool())\n        .logLevelWorkQueueSaturation(Level.INFO)\n        .build())\n    // Lower the log level when a task fails temporarily from the default WARN.\n    .logLevelTemporaryFailure(Level.INFO)\n    // 10 attempts at a task before blocking it.\n    .blockAfterAttempts(10)\n    // When calling flush(), select 0.5m records at a time.\n    .flushBatchSize(500_000)\n    // Flush once every 15 minutes only\n    .attemptFrequency(Duration.ofMinutes(15))\n    // Include Slf4j's Mapped Diagnostic Context in tasks. This means that anything in the MDC when schedule()\n    // is called will be recreated in the task when it runs. Very useful for tracking things like user ids and\n    // request ids across invocations.\n    .serializeMdc(true)\n    // Sets how long we should keep records of requests with a unique request id so duplicate requests\n    // can be rejected. Defaults to 7 days.\n    .retentionThreshold(Duration.ofDays(1))\n    // We can intercept and modify numerous events. The most common use is to catch blocked tasks\n    // and raise alerts for these to be investigated. A Slack interactive message is particularly effective here\n    // since it can be wired up to call unblock() automatically.\n    .listener(new TransactionOutboxListener() {\n\n      @Override\n      public void success(TransactionOutboxEntry entry) {\n        eventPublisher.publish(new OutboxTaskProcessedEvent(entry.getId()));\n      }\n\n      @Override\n      public void blocked(TransactionOutboxEntry entry, Throwable cause) {\n        eventPublisher.publish(new BlockedOutboxTaskEvent(entry.getId()));\n      }\n\n      @Override\n      public Map\u003cString, String\u003e extractSession() {\n        return Map.of();\n      }\n\n      @Override\n      public void wrapInvocationAndInit(Invocator invocator) {\n        invocator.runUnchecked();\n      }\n\n      @Override\n      public void wrapInvocation(Invocator invocator) throws Exception {\n        invocator.run();\n      }\n    })\n    .build();\n\n// Usage example, using the in-built transaction manager\nMDC.put(\"SESSIONKEY\", \"Foo\");\ntry {\n  transactionManager.inTransaction(tx -\u003e {\n    writeSomeChanges(tx.connection());\n    outbox.schedule(getClass())\n        .performRemoteCall(SaleType.SALE, Money.of(10, Currency.getInstance(\"USD\")));\n  });\n} finally {\n  MDC.clear();\n}\n```\n\n## Stubbing in tests\n\n`TransactionOutbox` should not be directly stubbed (e.g. using Mockito); the contract is too complex to stub out.\n\nInstead, [stubs](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/StubThreadLocalTransactionManager.html) [exist](https://www.javadoc.io/doc/com.gruelbox/transactionoutbox-core/latest/com/gruelbox/transactionoutbox/StubPersistor.html) for the various arguments to the builder, allowing you to build a `TransactionOutbox` with minimal external dependencies which can be called and verified in tests.\n\n```java\n// GIVEN\n\nSomeService mockService = Mockito.mock(SomeService.class);\n\n// Also see StubParameterContextTransactionManager\nTransactionManager transactionManager = new StubThreadLocalTransactionManager();\n\nTransactionOutbox outbox = TransactionOutbox.builder()\n    .instantiator(Instantiator.using(clazz -\u003e mockService)) // Return our mock\n    .persistor(StubPersistor.builder().build()) // Doesn't save anything\n    .submitter(Submitter.withExecutor(MoreExecutors.directExecutor())) // Execute all work in-line\n    .clockProvider(() -\u003e\n        Clock.fixed(LocalDateTime.of(2020, 3, 1, 12, 0)\n            .toInstant(ZoneOffset.UTC), ZoneOffset.UTC)) // Fix the clock (not necessary here)\n    .transactionManager(transactionManager)\n    .build();\n\n// WHEN\ntransactionManager.inTransaction(tx -\u003e\n   outbox.schedule(SomeService.class).doAThing(1));\n\n// THEN\nMockito.verify(mockService).doAThing(1);\n```\n\nDepending on the type of test, you may wish to use a real `Persistor` such as `DefaultPersistor` (if there's a real database available) or a real, multi-threaded `Submitter`. That's up to you.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgruelbox%2Ftransaction-outbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgruelbox%2Ftransaction-outbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgruelbox%2Ftransaction-outbox/lists"}