{"id":26594272,"url":"https://github.com/q3769/conseq4j","last_synced_at":"2025-07-05T17:33:56.099Z","repository":{"id":39611973,"uuid":"417881998","full_name":"q3769/conseq4j","owner":"q3769","description":"A Java concurrent API to asynchronously execute related tasks sequentially, and unrelated tasks concurrently.","archived":false,"fork":false,"pushed_at":"2023-11-05T15:01:15.000Z","size":978,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-05-07T18:05:54.639Z","etag":null,"topics":["asynchronous-tasks-orchestration","complex-event-processing","concurrency","concurrent-message-consumer","event-correlation","event-driven","java","java-concurrency-control","java-messaging","java-multithreading","java-parallel-processing","java-thread-management","message-oriented-middleware","sequencer","sequential-message-consumer","thread-affinity"],"latest_commit_sha":null,"homepage":"https://q3769.github.io/conseq4j/","language":"Java","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/q3769.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":"q3769","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2021-10-16T16:19:13.000Z","updated_at":"2024-01-12T18:23:29.000Z","dependencies_parsed_at":"2023-09-28T19:45:55.899Z","dependency_job_id":"af2c222b-d65f-493f-a69c-96bcf4d50b37","html_url":"https://github.com/q3769/conseq4j","commit_stats":{"total_commits":1202,"total_committers":7,"mean_commits":"171.71428571428572","dds":0.4217970049916805,"last_synced_commit":"14c5e9cfd7f86c3063a3cc7b6193ace9ade67c6f"},"previous_names":["q3769/qlib-conseq","q3769/conseq"],"tags_count":123,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/q3769%2Fconseq4j","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/q3769%2Fconseq4j/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/q3769%2Fconseq4j/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/q3769%2Fconseq4j/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/q3769","download_url":"https://codeload.github.com/q3769/conseq4j/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245127937,"owners_count":20565203,"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":["asynchronous-tasks-orchestration","complex-event-processing","concurrency","concurrent-message-consumer","event-correlation","event-driven","java","java-concurrency-control","java-messaging","java-multithreading","java-parallel-processing","java-thread-management","message-oriented-middleware","sequencer","sequential-message-consumer","thread-affinity"],"created_at":"2025-03-23T15:52:23.947Z","updated_at":"2025-07-05T17:33:56.092Z","avatar_url":"https://github.com/q3769.png","language":"Java","readme":"[![Maven Central](https://img.shields.io/maven-central/v/io.github.q3769/conseq4j.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.q3769%22%20AND%20a:%22conseq4j%22)\n\n# conseq4j\n\nA Java concurrent API to asynchronously execute related tasks sequentially, and unrelated tasks concurrently.\n\n- *conseq* is short for *con*current *seq*uencer.\n\n## User stories\n\n1. As an API client, I want to summon a sequential task executor by a sequence key, so that all the tasks sequentially\n   submitted under the same sequence key will be executed by the same executor in the same order as submitted;\n   meanwhile, the tasks with different sequence keys can be executed concurrently by different executors even when\n   submitted sequentially.\n2. As an API client, I want to asynchronously submit a task for execution together with a sequence key, so that, across\n   all such submissions, tasks submitted sequentially under the same sequence key are executed in the same order as\n   submitted; meanwhile, tasks of different sequence keys are executed concurrently even when submitted sequentially.\n\nConsider using conseq4j to achieve asynchronous concurrent processing globally while preserving meaningful local\nexecution order at the same time.\n\n## Prerequisite\n\n- Java 8+ for versions before 20230922.20230925.0 (exclusive)\n- Java 21+ for versions after 20230922.20230925.0 (inclusive)\n\n## Get it...\n\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.q3769/conseq4j.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.q3769%22%20AND%20a:%22conseq4j%22)\n\nInstall as a compile-scope dependency in Maven or other build tools alike.\n\n```xml\n\n\u003cdependency\u003e\n   \u003cgroupId\u003eio.github.q3769\u003c/groupId\u003e\n   \u003cartifactId\u003econseq4j\u003c/artifactId\u003e\n   \u003cversion\u003e...\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n## Use it...\n\nSome general notes:\n\n- Sequence keys\n\nA sequence key cannot be `null`. Any two keys, `sequenceKey1` and `sequenceKey2`, are considered \"the same sequence key\"\nif and only if `Objects.equals(sequenceKey1, sequenceKey2)` returns `true`.\n\n- Thread safety\n\nA conseq4j instance is thread-safe in and of itself. The usual thread-safety rules and concerns, however, still apply\nwhen programming the executable tasks. Moreover, in the context of concurrency and sequencing, the thread-safety concern\ngoes beyond concurrent modification of individual-task data, into that of meaningful execution order among multiple\nrelated tasks.\n\n- Concurrency and sequencing\n\nFirst of all, by definition, there is no such thing as order or sequence among tasks submitted concurrently by different\nthreads. No particular execution order is guaranteed on those concurrent tasks, regardless of their sequence keys. The\nconseq4j API only manages sequentially-submitted tasks - those that are submitted by a single thread, or by each single\nthread in case of multi-threading. To execute those sequential tasks, the conseq4j API provides both concurrency and\nsequencing: The tasks will be executed sequentially if they have the same sequence key, and concurrently if they have\ndifferent sequence keys.\n\nTechnically, to form a sequence, the client task-submitting thread only needs to be \"logically\" single. It does not\nalways have to be the same physical thread e.g. sometimes one thread may need to be replaced by another for various\nreasons. The conseq4j API should function correctly as long as the related tasks are submitted by at most one thread at\nany time, and with the right order of submission sequence over time. Fortunately, that is often naturally the case for\nthe API client, e.g. when the task submission is managed by a messaging provider such as Kafka, JMS, x-MQ, TIBCO EMS,\netc...\n\n### Style 1: summon a sequential executor by its sequence key, then use the executor as with a JDK `ExecutorService`\n\n- API\n\n```java\n\n@FunctionalInterface\npublic interface ExecutorServiceFactory {\n  /**\n   * Produces sequential executor for the specified sequence key. All calls with the same sequence\n   * key argument return the same executor. For concurrency, calls with different sequence key\n   * arguments may return different executors that can run in parallel.\n   *\n   * @param sequenceKey an {@link Object} instance whose hash code is used to summon the\n   *     corresponding executor.\n   * @return the sequential executor of type {@link java.util.concurrent.ExecutorService} that\n   *     executes all tasks of this sequence key in the same order as they are submitted.\n   */\n  ExecutorService getExecutorService(Object sequenceKey);\n}\n```\n\nThis API style loosely takes the form of \"thread affinity\". Sequence keys are used to summon executors of JDK\ntype [ExecutorService](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html). The same\nsequence key always gets back the same sequential executor. All tasks of that sequence key can then be \"affined\" to and\nexecuted sequentially by the summoned executor in the same submission order.\n\nThe total number of executors concurrently available at runtime is configurable. As each executor is sequential, the\nnumber of available executors equals the number of tasks that can be executed in parallel.\n\nConsider using this style when the summoned executor needs to provide\nthe [syntax and semantic richness](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html#method.summary)\nof the JDK `ExecutorService` API.\n\n- Sample usage\n\n```java\npublic class MessageConsumer {\n  /**\n   * Default conseq's concurrency is java.lang.Runtime.availableProcessors.\n   * \u003cp\u003e\n   * Or to set the global concurrency to 10, for example:\n   * \u003ccode\u003e\n   * private ExecutorServiceFactory conseqExecutorFactory = ConseqFactory.instance(10);\n   * \u003c/code\u003e\n   */\n  private final ExecutorServiceFactory conseqFactory = ConseqFactory.instance();\n\n  @Autowired\n  private ShoppingEventProcessor shoppingEventProcessor;\n\n  /**\n   * Suppose run-time invocation of this method is managed by the messaging provider. This is usually via a single \n   * caller thread.\n   * \u003cp\u003e\n   * Concurrency is achieved when shopping events of different shopping cart IDs are processed in parallel, by \n   * different executors. Sequence is maintained on all shopping events of the same shopping cart ID, by the same \n   * executor.\n   */\n  public void onMessage(Message shoppingEvent) {\n    conseqFactory.getExecutorService(shoppingEvent.getShoppingCartId())\n        .execute(() -\u003e shoppingEventProcessor.process(shoppingEvent));\n  }\n}\n```\n\nThe implementation of this thread-affinity style relies on hashing of the sequence keys into a fixed number of\n\"buckets\". These buckets are each associated with a sequential executor. The same sequence key is always hashed to and\nsummons back the same executor. Single-threaded, each executor ensures the execution order of all its tasks is the\nsame as they are submitted; excessive tasks pending execution are buffered in a FIFO task queue. Thus, the total\nnumber of buckets (i.e. the max number of available executors and the general concurrency) is the maximum number of\ntasks that can be executed in parallel at any given time.\n\nAs with hashing, collision may occur among different sequence keys. When hash collision happens, tasks of different\nsequence keys are assigned to the same executor. Due to the single-thread setup, the executor still ensures the local\nsequential execution order for each individual sequence key's tasks. However, unrelated tasks of different sequence\nkeys now assigned to the same bucket/executor may delay each other's execution inadvertently while waiting in the\nexecutor's task queue. Consider this a trade-off of the executor's having the same syntax and semantic richness as a\nJDK [ExecutorService](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html).\n\nTo account for hash collision, conseq4j Style 1 does not support any shutdown action on the API-provided\nexecutor ([ExecutorService](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html))\ninstance. That is to prevent unintended task cancellation across different sequence keys.\nThe [Future](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html) instance(s) subsequently\nreturned by the executor, though, is still cancellable. The hash collision may not be an issue for workloads that are\nasynchronous and focused on overall through-put, but is something to be aware of.\n\nThe default general concurrency is the JVM\nrun-time's [availableProcessors](https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors--):\n\n  ```jshelllanguage\n  ConseqFactory.instance();\n  ```\n\nThe concurrency can be customized:\n\n  ```jshelllanguage\n  ConseqFactory.instance(10)\n  ```\n\n### Style 2: submit each task directly for execution, together with its sequence key\n\n- API\n\n```java\n\n@FunctionalInterface\npublic interface TaskExecutor {\n  /**\n   * Asynchronously executes specified command in sequence regulated by specified key while\n   * providing concurrent execution capability to other tasks with different keys.\n   *\n   * @param command the Runnable task to run sequentially with others under the same sequence key\n   * @param sequenceKey the key under which all tasks are executed sequentially\n   * @return future holding run status of the submitted command\n   */\n  default Future\u003cVoid\u003e execute(Runnable command, Object sequenceKey) {\n    return submit(Executors.callable(command, null), sequenceKey);\n  }\n\n  /**\n   * Asynchronously executes specified task in sequence regulated by specified key while providing\n   * concurrent execution capability to other tasks with different keys.\n   *\n   * @param \u003cT\u003e the type of the task's result\n   * @param task the Callable task to run sequentially with others under the same sequence key\n   * @param sequenceKey the key under which all tasks are executed sequentially\n   * @return a Future representing pending completion of the submitted task\n   */\n  \u003cT\u003e Future\u003cT\u003e submit(Callable\u003cT\u003e task, Object sequenceKey);\n}\n```\n\nThis API style is more concise. Bypassing the JDK ExecutorService API, it services the submitted task directly. The same\nexecution semantics holds: Tasks of the same sequence key are executed in the same submission order; tasks of different\nsequence keys are managed to execute in parallel.\n\nFor versions requiring Java 21+, conseq4j Style 2 defaults to have no preset limit on the overall concurrency when\nexecuting tasks; for other versions, this style's default concurrency is the number of JVM run-time'\ns [availableProcessors](https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors--). Prefer\nthis style when the full-blown syntax and semantic support of\nJDK [ExecutorService](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html) is not\nrequired.\n\n- Sample usage\n\n```java\nimport conseq4j.execute.ConseqExecutor;\n\npublic class MessageConsumer {\n\n  /**\n   * Default executor has no preset limit on overall concurrency, running on virtual thread per task.\n   */\n  private final TaskExecutor conseqExecutor = ConseqExecutor.instance();\n\n  @Autowired\n  private ShoppingEventProcessor shoppingEventProcessor;\n\n  /**\n   * Suppose run-time invocation of this method is managed by the messaging provider. This is usually via a single\n   * caller thread.\n   * \u003cp\u003e\n   * Concurrency is achieved when shopping events of different shopping cart IDs are processed in parallel by\n   * different backing threads. Sequence is maintained for all shopping events of the same shopping cart ID, via\n   * linear progression of execution stages with {@link java.util.concurrent.CompletableFuture}.\n   */\n  public void onMessage(Message shoppingEvent) {\n    conseqExecutor.execute(() -\u003e shoppingEventProcessor.process(shoppingEvent), shoppingEvent.getShoppingCartId());\n  }\n}\n```\n\nThe implementation relies on\nJDK's [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html) to\nachieve sequential execution of related tasks. One single pool of threads is used to facilitate the overall\nasynchronous execution. The concurrency to execute unrelated tasks is generally limited only by the backing work\nthread pool's capacity, or unlimited in the case of default/virtual thread mode.\n\nInstead of thread-affinity or bucket hashing, tasks are decoupled from their execution threads. All pooled threads are\nanonymous and interchangeable to execute any tasks. Even sequential tasks of the same sequence key may be executed by\ndifferent threads, albeit in sequential order. When the work thread pool has idle threads available, a task awaiting\nexecution must have been blocked only by its own related task(s) of the same sequence key - as is necessary, and not\nby unrelated tasks of different sequence keys in the same \"bucket\" - as is unnecessary. This can be a desired\nadvantage over the thread-affinity API style, at the trade-off of lesser syntax and semantic richness than the\nJDK [ExecutorService](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html).\n\nFor versions requiring Java 21+, the default ConseqExecutor instance uses Virtual thread per task, and has no preset\nlimit on overall concurrency. For other versions, the default concurrency is the number of JVM's available processors.\n\n  ```jshelllanguage\n  ConseqExecutor.instance()\n  ```\n\nThe concurrency/parallelism can be customized and limited:\n\n  ```jshelllanguage\n  ConseqExecutor.instance(10)\n  ```\n\nThe `ConseqExecutor` instance can also use a fully-customized (Virtual or Platform thread-based) `ExecutorService` to\npower its async operations, e.g. with a fixed sized platform-thread pool:\n\n  ```jshelllanguage\n  ConseqExecutor.instance(Executors.newFixedThreadPool(10))\n  ```\n\n## Full disclosure - Asynchronous Conundrum\n\nThe Asynchronous Conundrum refers to the fact that asynchronous concurrent processing and deterministic order of\nexecution do not come together naturally; in asynchronous systems, certain limits and impedance mismatch exist between\nmaintaining meaningful local execution order and maximizing global concurrency.\n\nIn asynchronous concurrent messaging, there are generally two approaches to achieve necessary order:\n\n### 1. Preventive\n\nThis is more on the technical level. Sometimes it is possible to prevent related messages from ever being executed\nout of order in a globally concurrent process. This implies:\n\n(1) The message producer ensures that messages are posted to the messaging provider in correct order.\n\n(2) The messaging provider ensures that messages are delivered to the message consumer in the same order they are\nreceived.\n\n(3) The message consumer ensures that related messages are processed in the same order, e.g. by using a\nsequence/correlation key as with this API.\n\n### 2. Curative\n\nThis is more on the business rule level. Sometimes preventative measures are either not possible or not worthwhile to\npursue. By the time messages arrive at the consumer, they may be intrinsically out of order. E.g. when the messages are\ncoming in from independent producers and sources, there may be no guarantee of correct ordering in the first place. In\nsuch cases, the message consumer may be able to take a curative approach, by applying business rules to restore\nnecessary order and properly handle the out-of-order messages.\n\nCompared to preventative measures, corrective ones can be much more complicated in terms of design, implementation and\nruntime performance. E.g. for each incoming event, it may help to do a stateful/historical look-up of all the data and\nother events that are related; this forms a correlated and collective information session of the incoming event. A\ncomprehensive review of such session can detect and determine if the incoming event is out of order per business rules;\ncorrective measures can then be taken to restore the right order, among other reactive actions. This may fall into the\nscope of [Complex Event Processing (CEP)](https://en.wikipedia.org/wiki/Complex_event_processing). State Machines can\nalso be a useful design in such scenario.\n","funding_links":["https://github.com/sponsors/q3769"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fq3769%2Fconseq4j","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fq3769%2Fconseq4j","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fq3769%2Fconseq4j/lists"}