diff --git a/.idea/encodings.xml b/.idea/encodings.xml
index b4b2816..ea77922 100644
--- a/.idea/encodings.xml
+++ b/.idea/encodings.xml
@@ -7,6 +7,10 @@
+
+
+
+
diff --git a/ordo-spring-boot-autoconfigure/pom.xml b/ordo-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 0000000..d819c7c
--- /dev/null
+++ b/ordo-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ com.jetlumen
+ ordo
+ 1.0-SNAPSHOT
+
+
+ ordo-spring-boot-autoconfigure
+
+
+ 21
+ 21
+ UTF-8
+ 3.3.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+
+
+ com.jetlumen
+ ordo-api
+ ${project.version}
+
+
+ com.jetlumen
+ ordo-core
+ ${project.version}
+
+
+ com.jetlumen
+ ordo-storage-jdbc
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.flywaydb
+ flyway-core
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.h2database
+ h2
+ test
+
+
+
+
\ No newline at end of file
diff --git a/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfiguration.java b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfiguration.java
new file mode 100644
index 0000000..c14b5cb
--- /dev/null
+++ b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfiguration.java
@@ -0,0 +1,98 @@
+package com.jetlumen.ordo.spring;
+
+import com.jetlumen.ordo.api.AssigneeResolver;
+import com.jetlumen.ordo.api.OrdoEngine;
+import com.jetlumen.ordo.api.TransactionExecutor;
+import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
+import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
+import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
+import com.jetlumen.ordo.core.DefaultOrdoEngine;
+import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
+import com.jetlumen.ordo.storage.jdbc.JdbcConnectionProvider;
+import com.jetlumen.ordo.storage.jdbc.JdbcProcessDefinitionRepository;
+import com.jetlumen.ordo.storage.jdbc.JdbcProcessInstanceRepository;
+import com.jetlumen.ordo.storage.jdbc.JdbcTransactionExecutor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization;
+import org.springframework.context.annotation.Bean;
+
+import javax.sql.DataSource;
+import java.time.Clock;
+
+/**
+ * Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is
+ * present. Any bean already defined by the application (e.g. a custom
+ * {@link AssigneeResolver} or repository implementation) takes precedence over the
+ * defaults created here.
+ */
+@AutoConfiguration
+@ConditionalOnProperty(prefix = "ordo", name = "enabled", havingValue = "true", matchIfMissing = true)
+@ConditionalOnClass(DataSource.class)
+@ConditionalOnBean(DataSource.class)
+@AutoConfigureAfter({DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class})
+@EnableConfigurationProperties(OrdoProperties.class)
+public class OrdoJdbcAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public Clock ordoClock() {
+ return Clock.systemUTC();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AssigneeResolver ordoAssigneeResolver() {
+ return AssigneeResolver.direct();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @DependsOnDatabaseInitialization
+ public JdbcConnectionProvider ordoJdbcConnectionProvider(DataSource dataSource) {
+ return new JdbcConnectionProvider(dataSource);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public TransactionExecutor ordoTransactionExecutor(JdbcConnectionProvider connectionProvider) {
+ return new JdbcTransactionExecutor(connectionProvider);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ProcessDefinitionRepository ordoProcessDefinitionRepository(JdbcConnectionProvider connectionProvider) {
+ return new JdbcProcessDefinitionRepository(connectionProvider);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ProcessInstanceRepository ordoProcessInstanceRepository(JdbcConnectionProvider connectionProvider) {
+ return new JdbcProcessInstanceRepository(connectionProvider);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ApprovalTaskRepository ordoApprovalTaskRepository(JdbcConnectionProvider connectionProvider) {
+ return new JdbcApprovalTaskRepository(connectionProvider);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public OrdoEngine ordoEngine(Clock ordoClock,
+ AssigneeResolver ordoAssigneeResolver,
+ TransactionExecutor ordoTransactionExecutor,
+ ProcessDefinitionRepository ordoProcessDefinitionRepository,
+ ProcessInstanceRepository ordoProcessInstanceRepository,
+ ApprovalTaskRepository ordoApprovalTaskRepository) {
+ return new DefaultOrdoEngine(ordoClock, ordoAssigneeResolver, ordoTransactionExecutor,
+ ordoProcessDefinitionRepository, ordoProcessInstanceRepository, ordoApprovalTaskRepository);
+ }
+}
diff --git a/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoProperties.java b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoProperties.java
new file mode 100644
index 0000000..b7d353d
--- /dev/null
+++ b/ordo-spring-boot-autoconfigure/src/main/java/com/jetlumen/ordo/spring/OrdoProperties.java
@@ -0,0 +1,19 @@
+package com.jetlumen.ordo.spring;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/** Configuration properties for the Ordo Spring Boot autoconfiguration. */
+@ConfigurationProperties(prefix = "ordo")
+public class OrdoProperties {
+
+ /** Whether the Ordo JDBC autoconfiguration should be applied. */
+ private boolean enabled = true;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}
diff --git a/ordo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ordo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f5adb01
--- /dev/null
+++ b/ordo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+com.jetlumen.ordo.spring.OrdoJdbcAutoConfiguration
diff --git a/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java b/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java
new file mode 100644
index 0000000..445c000
--- /dev/null
+++ b/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java
@@ -0,0 +1,86 @@
+package com.jetlumen.ordo.spring;
+
+import com.jetlumen.ordo.api.ApprovalStep;
+import com.jetlumen.ordo.api.ApprovalTask;
+import com.jetlumen.ordo.api.AssigneeResolver;
+import com.jetlumen.ordo.api.OrdoEngine;
+import com.jetlumen.ordo.api.ProcessDefinition;
+import com.jetlumen.ordo.api.ProcessInstance;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OrdoJdbcAutoConfigurationTest {
+
+ private static final ProcessDefinition LEAVE_REQUEST = new ProcessDefinition("leave-request", "Leave request",
+ List.of(new ApprovalStep("manager", "Manager approval", "maria")));
+
+ private final ApplicationContextRunner withDataSourceRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(
+ DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class, OrdoJdbcAutoConfiguration.class))
+ .withPropertyValues(
+ "spring.datasource.url=jdbc:h2:mem:ordo_" + UUID.randomUUID()
+ + ";MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
+ "spring.datasource.username=sa");
+
+ private final ApplicationContextRunner withoutDataSourceRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(OrdoJdbcAutoConfiguration.class));
+
+ @Test
+ void assemblesJdbcBackedEngineWhenDataSourceIsPresent() {
+ withDataSourceRunner.run(context -> {
+ assertThat(context).hasSingleBean(OrdoEngine.class);
+ OrdoEngine engine = context.getBean(OrdoEngine.class);
+
+ engine.register(LEAVE_REQUEST);
+ ProcessInstance instance = engine.start("leave-request", "alice");
+ List pending = engine.findPendingTasksByAssignee("maria");
+ assertThat(pending).hasSize(1);
+
+ ApprovalTask approved = engine.approve(pending.get(0).id(), "maria", "looks good");
+ assertThat(approved.status().name()).isEqualTo("APPROVED");
+ assertThat(engine.findInstance(instance.id())).isPresent();
+ });
+ }
+
+ @Test
+ void backsOffWhenNoDataSourceBeanIsPresent() {
+ withoutDataSourceRunner.run(context ->
+ assertThat(context).doesNotHaveBean(OrdoEngine.class));
+ }
+
+ @Test
+ void backsOffWhenDisabledViaProperty() {
+ withDataSourceRunner.withPropertyValues("ordo.enabled=false")
+ .run(context ->
+ assertThat(context).doesNotHaveBean(OrdoEngine.class));
+ }
+
+ @Test
+ void honoursUserDefinedAssigneeResolver() {
+ withDataSourceRunner.withUserConfiguration(CustomAssigneeResolverConfig.class)
+ .run(context -> {
+ assertThat(context).hasSingleBean(OrdoEngine.class);
+ assertThat(context.getBean(AssigneeResolver.class)).isNotNull();
+ assertThat(context.getBean(AssigneeResolver.class).resolve(null, null))
+ .isEqualTo("custom-assignee");
+ });
+ }
+
+ @Configuration
+ static class CustomAssigneeResolverConfig {
+ @Bean
+ AssigneeResolver ordoAssigneeResolver() {
+ return (step, ctx) -> "custom-assignee";
+ }
+ }
+}
diff --git a/ordo-spring-boot-starter/pom.xml b/ordo-spring-boot-starter/pom.xml
new file mode 100644
index 0000000..ef9e4d8
--- /dev/null
+++ b/ordo-spring-boot-starter/pom.xml
@@ -0,0 +1,63 @@
+
+
+ 4.0.0
+
+ com.jetlumen
+ ordo
+ 1.0-SNAPSHOT
+
+
+ ordo-spring-boot-starter
+
+
+ 21
+ 21
+ UTF-8
+ 3.3.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+
+
+ com.jetlumen
+ ordo-spring-boot-autoconfigure
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.flywaydb
+ flyway-core
+
+
+
+ org.flywaydb
+ flyway-database-postgresql
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 87dbdba..67ca131 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,8 @@
ordo-api
ordo-example
ordo-storage-jdbc
+ ordo-spring-boot-starter
+ ordo-spring-boot-autoconfigure