DBOS Transact for Java

repository·main·Indexed 42 days ago

https://github.com/dbos-inc/dbos-transact-java

A lightweight Java library for building durable, reliable workflows using Postgres as a checkpointing backend. It enables long-running processes to recover from failures without external orchestrators via @Workflow annotations and DBOS.runStep(). Features include asynchronous workflow execution, durable queues for distributed tasks, cron scheduling, and durable notifications. Includes a Spring Boot starter providing the @TransactionalStep annotation for idempotent transactional steps compatible with Spring JDBC, JDBI, jOOQ, JPA, and MyBatis.

Tokens
4K
Snippets
12
Records
15
Agent score
54%

What's inside DBOS Transact

  1. Use Durable Queues for distributed tasks

    main

    DBOS queues allow you to durably run distributed tasks using Postgres as the backend (no separate broker required). You can enqueue tasks from a durable workflow, and DBOS guarantees task completion and result retrieval even if the application is interrupted.

    Key features include:

    • Flow control (concurrency limits per queue or process)
    • Task timeouts
    • Rate limiting
    • Task deduplication
    • Task prioritization

    To use queues, you must first register the queue in your main application and then use StartWorkflowOptions to associate a workflow with that queue.

    // In your main
    var queue = new Queue("exampleQueue");
    DBOS.registerQueue(queue);
    
    // Enqueuing a task
    public void queuedTasks() {
        for (int i = 0; i < 3; i++) {
            String workflowId = "child" + i;
            var options = new StartWorkflowOptions(workflowId).withQueue(q);
            List<WorkflowHandle<String>> handles = new ArrayList<>();
            handles.add(DBOS.startWorkflow(()->simpleService.childWorkflow(workflowId), options));
        }
    
        for (int i = 0 ; i < 3 ; i++) {
            String workflowId = "child"+i;
            var h = DBOS.retrieveWorkflow(workflowId);
            System.out.println(h.getResult());
        }
    }
  2. Use the @TransactionalStep annotation

    main

    The @TransactionalStep annotation marks a Spring-managed method as an idempotent transactional step within a DBOS workflow.

    Key behaviors:

    • Each annotated method runs in a REQUIRES_NEW Spring transaction.
    • The method's return value is written to a tx_step_outputs table atomically with the database work performed inside the method.
    • On workflow retry, DBOS checks the tx_step_outputs table; if a result exists, it is replayed without re-executing the method body.

    Requirement: The annotated method must be called through a Spring proxy. You should inject the bean containing the @TransactionalStep method into a @Workflow-annotated method in a different Spring bean.

    @Service
    public class OrderStepService {
        @Autowired JdbcTemplate jdbc;
    
        @TransactionalStep
        public Order saveOrder(Order order) {
            jdbc.update("INSERT INTO orders(id, item, qty) VALUES (?, ?, ?)",
                order.id(), order.item(), order.qty());
            return order;
        }
    }
    
    @Service
    public class OrderWorkflowService {
        @Autowired OrderStepService steps;
    
        @Workflow
        public Order processOrder(Order order) {
            return steps.saveOrder(order);
        }
    }
  3. Use Durable Notifications for event-driven workflows

    main

    Durable notifications allow you to pause a workflow until a specific event is received or to emit progress updates to external clients. All notifications are stored in Postgres, providing exactly-once semantics. You can set durable timeouts so that a workflow resumes either when a notification arrives or when the timeout is reached, even across restarts.

    @Workflow(name = "billing")
    public void billingWorkflow() {
        // Calculate the charge, then submit the bill to a payments service
        String paymentStatus = (String) DBOS.recv(PAYMENT_STATUS, paymentServiceTimeout);
        if (paymentStatus.equals("paid")) {
            // handle paid
        } else {
            // handle not paid
        }
    }
    
    @Workflow(name = "payment") 
    public void payment(String targetWorkflowId) {
        DBOS.send(targetWorkflowId, PAYMENT_STATUS, "paid") ;
    }
  4. What are DBOS Durable Workflows?

    main

    DBOS provides lightweight durable workflows built on top of Postgres. It allows you to write long-lived, reliable code that can survive crashes, restarts, and failures without losing state or duplicating work.

    As workflows run, DBOS checkpoints each step in a Postgres database. If a process stops (due to failure, intentional suspension, or machine death), the program can recover from these checkpoints to restore its exact state and continue from where it left off. This is achieved by registering ordinary functions as workflows and steps using the @Workflow annotation and DBOS.runStep() method.

    interface Example {
        public void workflow();
    }
    
    class ExampleImpl implements Example {
    
        private void stepOne() {
            System.out.println("Step one completed!");
        }
    
        private void stepTwo() {
            System.out.println("Step two completed!");
        }
    
        @Workflow
        public void workflow() {
            DBOS.runStep(() -> stepOne(), "stepOne");
            DBOS.runStep(() -> stepTwo(), "stepTwo");
        }
    }
  5. Schedule workflows with Cron or Durable Sleep

    main

    You can schedule workflows using cron syntax via the @Scheduled annotation or use durable sleep to pause workflows for extended periods (days or weeks).

    To enable scheduling, you must register your workflow implementation using DBOS.registerWorkflows().

    public class SchedulerImpl implements Scheduler {
        
        @Workflow(name = "every5Second")
        @Scheduled(cron = "0/5 * * * * ?")
        public void every5Second(Instant schedule , Instant actual) {
            log.info("Executed workflow  "+  schedule.toString() + "   " + actual.toString()) ;
        }
    }
    
    // In your main
    DBOS.registerWorkflows(Scheduler.class, new SchedulerImpl());
  6. Import DBOS Transact into your application

    main

    To use the DBOS Transact Java SDK in your project, add the following dependencies to your build.gradle.kts file. Note that logback-classic is required for logging.

    implementation("dev.dbos:transact")      
    implementation("ch.qos.logback:logback-classic:1.5.6")
  7. Format code using Spotless

    main

    The SDK uses Spotless for consistent code formatting. Spotless checks formatting automatically during the build process. If the build fails due to formatting violations, run the spotlessApply Gradle task to automatically update the code to the correct format.

    ./gradlew spotlessApply
  8. Run workflows asynchronously

    main

    You can run workflows in the background without changing their interface or implementation. This is useful for long-running tasks where the caller needs to return immediately and check the status or retrieve the result later.

    var handle = DBOS.startWorkflow(()->example.exampleWorkflow("HelloDBOS"));
    result = handle.getResult();
  9. Set up the development environment for DBOS Transact

    main

    To develop with the DBOS Transact Java SDK, ensure you have the following installed and configured:

    1. JDK: OpenJDK 21 (recommended).
    2. IDE: IntelliJ (Community edition is sufficient), VSCode, or vi.
    3. Database: A Postgres Docker container with the following configuration:
      • Host: localhost
      • Port: 5432
      • User: postgres
    4. Environment Variable: Set PGPASSWORD to the password for your postgres user.
    export PGPASSWORD=your_postgres_password
  10. Install transact-spring-txstep-starter

    main

    To use @TransactionalStep in a Spring Boot application, you must include both the DBOS Spring Boot starter and the transact-spring-txstep-starter module in your dependencies.

    ### Gradle
    ```kotlin
    implementation("dev.dbos:transact-spring-boot-starter:<version>")
    implementation("dev.dbos:transact-spring-txstep-starter:<version>")

    Maven

    <dependency>
      <groupId>dev.dbos</groupId>
      <artifactId>transact-spring-boot-starter</artifactId>
      <version>VERSION</version>
    </dependency>
    <dependency>
      <groupId>dev.dbos</groupId>
      <artifactId>transact-spring-txstep-starter</artifactId>
      <version>VERSION</version>
    </dependency>
  11. Configure @TransactionalStep with Spring JDBC, JDBI, jOOQ, JPA, or MyBatis

    main

    The transact-spring-txstep-starter integrates with common Spring data access technologies. Because it uses Spring's transaction management, these tools will automatically participate in the REQUIRES_NEW transaction started by the @TransactionalStep aspect.

    Spring JDBC / JdbcTemplate

    Works out-of-the-box. JdbcTemplate uses DataSourceUtils internally to join the active transaction.

    JDBI

    Requires jdbi3-spring so that SpringTransactionHandler reuses the active Spring transaction.

    jOOQ

    Add spring-boot-starter-jooq and set spring.jooq.sql-dialect=POSTGRES. The starter wraps the DataSource in a TransactionAwareDataSourceProxy so jOOQ queries execute on the transaction-bound connection.

    JPA / Hibernate

    Works with spring-boot-starter-data-jpa. Both Spring Data JPA repositories and direct EntityManager access (via EntityManagerFactoryUtils.getTransactionalEntityManager(emf)) are supported.

    MyBatis

    Works with mybatis-spring-boot-starter. MyBatis mappers participate in Spring transactions automatically via SqlSessionTemplate.

    ### JDBI Example
    ```java
    @Configuration
    public class JdbiConfig {
        @Bean
        public Jdbi jdbi(DataSource dataSource) throws Exception {
            var factory = new JdbiFactoryBean(dataSource);
            factory.afterPropertiesSet();
            return factory.getObject();
        }
    }
    
    @Service
    public class OrderStepService {
        @Autowired Jdbi jdbi;
    
        @TransactionalStep
        public Order saveOrder(Order order) {
            jdbi.withHandle(h ->
                h.execute("INSERT INTO orders(id, item, qty) VALUES (?, ?, ?)",
                    order.id(), order.item(), order.qty()));
            return order;
        }
    }

    jOOQ Example

    @Service
    public class OrderStepService {
        @Autowired DSLContext dsl;
    
        @TransactionalStep
        public Order saveOrder(Order order) {
            dsl.execute("INSERT INTO orders(id, item, qty) VALUES (?, ?, ?)",
                    order.id(), order.item(), order.qty());
            return order;
        }
    }

    JPA Example

    @Service
    public class OrderStepService {
        @Autowired OrderRepository repo;
    
        @TransactionalStep
        public Order saveOrder(Order order) {
            return repo.save(order);
        }
    }

    MyBatis Example

    @Mapper
    public interface OrderMapper {
        @Insert("INSERT INTO orders(id, item, qty) VALUES(#{id}, #{item}, #{qty})")
        void insert(@Param("id") String id, @Param("item") String item, @Param("qty") int qty);
    }
    
    @Service
    public class OrderStepService {
        @Autowired OrderMapper orderMapper;
    
        @TransactionalStep
        public Order saveOrder(Order order) {
            orderMapper.insert(order.id(), order.item(), order.qty());
            return order;
        }
    }