├── src ├── test │ └── java │ │ └── br │ │ └── com │ │ └── celfons │ │ ├── domains │ │ ├── Product.kt │ │ └── CheckoutWorkflow.kt │ │ ├── services │ │ ├── ClientService.kt │ │ └── PaymentService.kt │ │ └── WorkflowTest.kt └── main │ └── java │ └── br │ └── com │ └── celfons │ ├── services │ └── WorkflowService.kt │ └── domains │ └── Workflow.kt ├── .gitignore ├── README.md └── pom.xml /src/test/java/br/com/celfons/domains/Product.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons.domains 2 | 3 | data class Product( 4 | var id: Int? = null, 5 | var name: String? = null, 6 | var goPay: Boolean? = true 7 | ) 8 | -------------------------------------------------------------------------------- /src/test/java/br/com/celfons/services/ClientService.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons.services 2 | 3 | import br.com.celfons.domains.CheckoutWorkflow 4 | import br.com.celfons.domains.Workflow 5 | import org.springframework.stereotype.Service 6 | 7 | @Service 8 | open class ClientService: WorkflowService() { 9 | 10 | override fun defaultCall(workflow: Workflow): CheckoutWorkflow = workflow as CheckoutWorkflow /* TODO("not implemented") */ 11 | 12 | override fun rollbackCall(workflow: Workflow): CheckoutWorkflow = workflow as CheckoutWorkflow /* TODO("not implemented") */ 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/br/com/celfons/services/PaymentService.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons.services 2 | 3 | import br.com.celfons.domains.CheckoutWorkflow 4 | import br.com.celfons.domains.Workflow 5 | import org.springframework.stereotype.Service 6 | 7 | @Service 8 | open class PaymentService: WorkflowService() { 9 | 10 | override fun defaultCall(workflow: Workflow): CheckoutWorkflow = workflow as CheckoutWorkflow /* TODO("not implemented") */ 11 | 12 | override fun rollbackCall(workflow: Workflow): CheckoutWorkflow = workflow as CheckoutWorkflow /* TODO("not implemented") */ 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/br/com/celfons/services/WorkflowService.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons.services 2 | 3 | import br.com.celfons.domains.Workflow 4 | import java.io.Serializable 5 | 6 | /* 7 | TODO extends to implements business rules 8 | */ 9 | 10 | abstract class WorkflowService : Serializable { 11 | 12 | fun call(workflow: Workflow): Workflow = workflow.takeIf { it.rollback!! } 13 | ?.run { rollbackCall(workflow) } 14 | ?: run { defaultCall(workflow) } 15 | 16 | abstract fun defaultCall(workflow: Workflow): Workflow 17 | 18 | abstract fun rollbackCall(workflow: Workflow): Workflow 19 | 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Intellij project files 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | 31 | #Gradle 32 | .gradletasknamecache 33 | .gradle/ 34 | gradle/ 35 | build/ 36 | bin/ 37 | out/ 38 | gradlew.bat 39 | 40 | classes/ 41 | 42 | target/ 43 | 44 | application-local.properties 45 | bootstrap-local.properties 46 | 47 | -------------------------------------------------------------------------------- /src/test/java/br/com/celfons/domains/CheckoutWorkflow.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons.domains 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | import org.springframework.stereotype.Component 7 | import org.springframework.transaction.annotation.Transactional 8 | import java.util.UUID 9 | 10 | @Component 11 | class CheckoutWorkflow( 12 | @Id override var id: UUID? = UUID.randomUUID(), /* Overridden to use annotation */ 13 | override var data: Product? = Product(), /* Overridden data type */ 14 | override var status: Status? = Status.INITIAL, /* Overridden status type */ 15 | @Autowired var repository: JpaRepository? = null 16 | ) : Workflow(id, data, status) { 17 | 18 | constructor(workflow: Workflow?) : this(workflow?.id, workflow?.data as? Product?, workflow?.status as? Status?) 19 | 20 | @Transactional 21 | override fun save(): Workflow? = this 22 | .apply { repository?.save(this) } 23 | .apply { println("saved: $this") } 24 | 25 | override fun rollbackError(): CheckoutWorkflow = this.apply { throw Exception() } 26 | 27 | override fun updateStatus(status: Any?): CheckoutWorkflow = this.apply { this.status = status as Status? } 28 | 29 | override fun failure(): Workflow? = save(Status.ERROR) 30 | 31 | override fun toString(): String { 32 | return "CheckoutWorkflow(id=$id, data=$data, status=$status, rollback=$rollback, creationDate=$creationDate, updatedDate=$updatedDate)" 33 | } 34 | 35 | enum class Status { 36 | INITIAL, 37 | PROCESSING, 38 | SUCCESS, 39 | ERROR 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SAGA'S OPEN SOURCE BY KOTLIN 2 | ============== 3 | 4 | # Use 5 | 6 | - By Maven Repository 7 | - Import GIT 8 | 9 | # Dependencies 10 | 11 | - Kotlin 1.3.71 12 | - Spring Boot Starter 2.2.6.RELEASE - test scope* 13 | 14 | --- 15 | 16 | ## Example 17 | 18 | ```Kotlin 19 | // code away! 20 | 21 | /* Extends Workflow class */ 22 | open class WorkflowTest( 23 | override var data: Object? = null /* Overridden data type to your context */ 24 | override var status: Object? = null /* Overridden status type to your context */ 25 | var repository: JpaRepository? = null /* Added repository to your context */ 26 | ) : Workflow(data = data, status = status) 27 | /* Optional implements rollback error flow and updated status flow */ 28 | 29 | @Service /* Build service class and implements business rules */ 30 | open class Service: WorkflowService() 31 | 32 | WorkflowTest() /* Execute sync or async and sequentially */ 33 | .save("INITIAL") /* Optional save workflow progress */ 34 | .flow(service, async = true) /* Inject your sync/async service */ 35 | .save("PROCESSING") 36 | .insideFlow { conditional -> conditional.takeIf { that -> (that?.data as Product).goPay!! } 37 | ?.flow(paymentService) } /* conditional flow */ 38 | .save("SUCCESS") 39 | /* Implements workflow rules */ 40 | ``` 41 | 42 | --- 43 | 44 | ## Test Coverage 45 | | Package Class | Class, % | Method, % | Line, % | 46 | | --------------| -------------|-------------------| -------------------| 47 | | all classes | 100% (3/ 3) | 50% (10/ 20) | 57% (23/ 40) | 48 | 49 | [![Build Status](http://img.shields.io/travis/badges/badgerbadgerbadger.svg?style=flat-square)](https://travis-ci.org/badges/badgerbadgerbadger) 50 | 51 | ## License 52 | - OPEN SOURCE 53 | 54 | ## Article PT-BR 55 | - https://www.linkedin.com/pulse/padr%C3%A3o-saga-para-arquitetura-de-microservi%C3%A7os-marcel-fonseca/ 56 | -------------------------------------------------------------------------------- /src/test/java/br/com/celfons/WorkflowTest.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons 2 | 3 | import br.com.celfons.domains.CheckoutWorkflow 4 | import br.com.celfons.domains.CheckoutWorkflow.Status.ERROR 5 | import br.com.celfons.domains.CheckoutWorkflow.Status.INITIAL 6 | import br.com.celfons.domains.CheckoutWorkflow.Status.PROCESSING 7 | import br.com.celfons.domains.CheckoutWorkflow.Status.SUCCESS 8 | import br.com.celfons.domains.Product 9 | import br.com.celfons.services.ClientService 10 | import br.com.celfons.services.PaymentService 11 | import org.junit.Assert 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | import org.mockito.Mock 15 | import org.mockito.junit.MockitoJUnitRunner 16 | import org.springframework.data.jpa.repository.JpaRepository 17 | import java.util.UUID 18 | 19 | @RunWith(MockitoJUnitRunner::class) 20 | open class WorkflowTest { 21 | 22 | @Mock 23 | lateinit var repository: JpaRepository 24 | 25 | @Mock 26 | lateinit var clientService: ClientService 27 | 28 | @Mock 29 | lateinit var paymentService: PaymentService 30 | 31 | @Test 32 | fun execute() { 33 | 34 | val product = Product(id = Integer.MAX_VALUE, name = String.toString()) 35 | 36 | val workflow = CheckoutWorkflow(data = product, repository = repository) 37 | .save(INITIAL) 38 | .flow(clientService, async = true) 39 | .save(PROCESSING) 40 | .insideFlow { conditional -> conditional.takeIf { that -> (that?.data as Product).goPay!! } 41 | ?.flow(paymentService) } 42 | .save(SUCCESS) 43 | 44 | /* 45 | 46 | TODO implements workflow rules 47 | 48 | */ 49 | 50 | Assert.assertEquals(SUCCESS, workflow.status) 51 | 52 | } 53 | 54 | @Test 55 | fun executeMultiplesFlows() { 56 | 57 | val product = Product(id = Integer.MAX_VALUE, name = String.toString()) 58 | 59 | val workflow1 = CheckoutWorkflow(data = product, repository = repository) 60 | .flow(clientService, async = true) 61 | .save(INITIAL) 62 | 63 | val workflow2 = repository.findById(workflow1.id!!) 64 | .takeIf { it.isPresent }?.get() 65 | ?.flow(paymentService) 66 | ?.save(PROCESSING) 67 | 68 | CheckoutWorkflow(workflow2) 69 | .insideFlow { 70 | conditional -> conditional.takeIf { that -> that?.rollback!! } 71 | ?.save(ERROR) ?: conditional?.save(SUCCESS) 72 | } 73 | 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/br/com/celfons/domains/Workflow.kt: -------------------------------------------------------------------------------- 1 | package br.com.celfons.domains 2 | 3 | import br.com.celfons.services.WorkflowService 4 | import kotlinx.coroutines.GlobalScope as Async 5 | import kotlinx.coroutines.launch as execute 6 | import java.time.LocalDateTime 7 | import java.time.LocalDateTime.now 8 | import java.util.UUID 9 | 10 | /* 11 | TODO extends to implements new workflow 12 | */ 13 | 14 | abstract class Workflow( 15 | open val id: UUID? = UUID.randomUUID(), 16 | open val data: Any? = null, /* TODO override data type */ 17 | open val status: Any? = null, 18 | open var rollback: Boolean? = false, 19 | open var creationDate: LocalDateTime? = now(), 20 | open var updatedDate: LocalDateTime? = now(), 21 | open var flows: MutableMap? = mutableMapOf() 22 | ) { 23 | 24 | constructor(workflow: Workflow?) : this() 25 | 26 | internal fun flow(service: WorkflowService, async: Boolean? = false): Workflow = 27 | runCatching { 28 | this.takeIf { !rollback!! } 29 | ?.run { flows?.put(service, async!!) } 30 | .run { call(service, async!!) } 31 | } 32 | .onFailure { failure()?.rollback() } 33 | .getOrDefault(this) 34 | 35 | internal fun save(status: Any?): Workflow = 36 | runCatching { 37 | this.apply { updatedDate = now() } 38 | .apply { updateStatus(status)?.save() } 39 | } 40 | .onFailure { failure() } 41 | .getOrDefault(this) 42 | 43 | private fun rollback(): Workflow = 44 | this.apply { rollback = true } 45 | .apply { 46 | flows?.toList()?.asReversed()?.forEach { 47 | runCatching { call(it.first, it.second) } 48 | .onFailure { rollbackError() } 49 | .getOrDefault(this) 50 | } 51 | } 52 | 53 | private fun call(service: WorkflowService, async: Boolean) = this.takeIf { async } 54 | ?.also { Async.execute { service.call(it) } } 55 | ?: also { service.call(it) } 56 | 57 | internal inline fun insideFlow(flow: (workflow: Workflow?) -> Unit): Workflow = this 58 | 59 | protected open fun failure(): Workflow? = this /* TODO implements to failure */ 60 | 61 | protected open fun updateStatus(status: Any? = null): Workflow? = this /* TODO implements update to workflow status */ 62 | 63 | protected open fun save(): Workflow? = this /* TODO implements to persist workflow */ 64 | 65 | protected open fun rollbackError(): Workflow? = this /* TODO implements rollback error */ 66 | 67 | } 68 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | br.com.celfons 8 | saga 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 1.3.71 13 | 1.3.7 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-parent 19 | 2.2.6.RELEASE 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.jetbrains.kotlin 27 | kotlin-stdlib-jdk8 28 | ${kotlin.version} 29 | 30 | 31 | 32 | org.jetbrains.kotlinx 33 | kotlinx-coroutines-core 34 | ${kotlinx.version} 35 | 36 | 37 | 38 | 39 | org.jetbrains.kotlin 40 | kotlin-test-junit 41 | ${kotlin.version} 42 | test 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-test 48 | test 49 | 50 | 51 | 52 | org.springframework.data 53 | spring-data-jpa 54 | test 55 | 56 | 57 | 58 | org.jetbrains.kotlin 59 | kotlin-stdlib-jdk8 60 | ${kotlin.version} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.jetbrains.kotlin 69 | kotlin-maven-plugin 70 | ${kotlin.version} 71 | 72 | 73 | compile 74 | compile 75 | 76 | compile 77 | 78 | 79 | 80 | test-compile 81 | test-compile 82 | 83 | test-compile 84 | 85 | 86 | 87 | 88 | 1.8 89 | 90 | 91 | 92 | 93 | 94 | 95 | --------------------------------------------------------------------------------