You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by ni...@apache.org on 2018/07/22 03:01:51 UTC

[incubator-servicecomb-saga] 01/02: SCB-692 Restore the old sage files

This is an automated email from the ASF dual-hosted git repository.

ningjiang pushed a commit to branch SCB-692
in repository https://gitbox.apache.org/repos/asf/incubator-servicecomb-saga.git

commit 2a19cf5a43dba3630bf666094baa489612384522
Author: Willem Jiang <ji...@huawei.com>
AuthorDate: Fri Jul 20 18:52:12 2018 +0800

    SCB-692 Restore the old sage files
---
 docs/api/api.md                                    | 168 +++++
 docs/old_saga.md                                   |  53 ++
 integration-tests/coverage-aggregate/pom.xml       |  20 +
 pom.xml                                            |  41 ++
 saga-core/pom.xml                                  | 103 +++
 .../servicecomb/saga/core/BackwardRecovery.java    |  49 ++
 .../apache/servicecomb/saga/core/Compensation.java |  33 +
 .../saga/core/CompensationTaskConsumer.java        |  69 ++
 .../servicecomb/saga/core/CompositeSagaLog.java    |  40 ++
 .../saga/core/CompositeSagaResponse.java           |  48 ++
 .../apache/servicecomb/saga/core/Descriptive.java  |  25 +
 .../apache/servicecomb/saga/core/EventContext.java |  28 +
 .../servicecomb/saga/core/EventEnvelope.java       |  47 ++
 .../apache/servicecomb/saga/core/EventStore.java   |  25 +
 .../servicecomb/saga/core/FailedSagaResponse.java  |  50 ++
 .../org/apache/servicecomb/saga/core/Fallback.java |  25 +
 .../servicecomb/saga/core/FallbackPolicy.java      |  62 ++
 .../servicecomb/saga/core/ForwardRecovery.java     |  53 ++
 .../servicecomb/saga/core/GraphBasedSaga.java      | 132 ++++
 .../apache/servicecomb/saga/core/IdGenerator.java  |  25 +
 .../saga/core/LoggingRecoveryPolicy.java           |  46 ++
 .../servicecomb/saga/core/LongIdGenerator.java     |  30 +
 .../servicecomb/saga/core/NoOpSagaRequest.java     | 107 ++++
 .../apache/servicecomb/saga/core/Operation.java    |  33 +
 .../servicecomb/saga/core/PersistentLog.java       |  22 +
 .../servicecomb/saga/core/PersistentStore.java     |  26 +
 .../servicecomb/saga/core/RecoveryPolicy.java      |  26 +
 .../servicecomb/saga/core/RequestProcessTask.java  |  67 ++
 .../servicecomb/saga/core/RestOperation.java       |  59 ++
 .../org/apache/servicecomb/saga/core/Saga.java     |  27 +
 .../apache/servicecomb/saga/core/SagaContext.java  |  36 ++
 .../servicecomb/saga/core/SagaContextImpl.java     | 133 ++++
 .../servicecomb/saga/core/SagaDefinition.java      |  25 +
 .../apache/servicecomb/saga/core/SagaEndTask.java  |  48 ++
 .../servicecomb/saga/core/SagaEndedEvent.java      |  38 ++
 .../apache/servicecomb/saga/core/SagaEvent.java    |  39 ++
 .../servicecomb/saga/core/SagaException.java       |  29 +
 .../org/apache/servicecomb/saga/core/SagaLog.java  |  22 +
 .../apache/servicecomb/saga/core/SagaRequest.java  |  48 ++
 .../servicecomb/saga/core/SagaRequestImpl.java     | 153 +++++
 .../apache/servicecomb/saga/core/SagaResponse.java |  61 ++
 .../saga/core/SagaStartFailedException.java        |  25 +
 .../servicecomb/saga/core/SagaStartTask.java       |  56 ++
 .../servicecomb/saga/core/SagaStartedEvent.java    |  46 ++
 .../apache/servicecomb/saga/core/SagaState.java    |  27 +
 .../org/apache/servicecomb/saga/core/SagaTask.java |  31 +
 .../servicecomb/saga/core/SagaTaskFactory.java     |  98 +++
 .../saga/core/SuccessfulSagaResponse.java          |  43 ++
 .../apache/servicecomb/saga/core/TaskConsumer.java |  29 +
 .../apache/servicecomb/saga/core/TaskRunner.java   |  72 +++
 .../apache/servicecomb/saga/core/ToJsonFormat.java |  26 +
 .../apache/servicecomb/saga/core/Transaction.java  |  27 +
 .../saga/core/TransactionAbortedEvent.java         |  57 ++
 .../saga/core/TransactionCompensatedEvent.java     |  56 ++
 .../saga/core/TransactionEndedEvent.java           |  56 ++
 .../saga/core/TransactionFailedException.java      |  29 +
 .../saga/core/TransactionStartedEvent.java         |  45 ++
 .../saga/core/TransactionTaskConsumer.java         | 123 ++++
 .../apache/servicecomb/saga/core/Transport.java    |  22 +
 .../saga/core/TransportFailedException.java        |  28 +
 .../saga/core/actors/ActorBasedSaga.java           |  65 ++
 .../saga/core/actors/ActorBasedSagaFactory.java    |  83 +++
 .../saga/core/actors/CompletionCallbackActor.java  |  69 ++
 .../saga/core/actors/EventContextImpl.java         |  60 ++
 .../servicecomb/saga/core/actors/RequestActor.java | 206 ++++++
 .../saga/core/actors/RequestActorBuilder.java      | 108 ++++
 .../saga/core/actors/RequestActorContext.java      |  82 +++
 .../saga/core/actors/messages/AbortMessage.java    |  37 ++
 .../core/actors/messages/AbortRecoveryMessage.java |  33 +
 .../core/actors/messages/CompensateMessage.java    |  32 +
 .../messages/CompensationRecoveryMessage.java      |  22 +
 .../saga/core/actors/messages/FailMessage.java     |  33 +
 .../saga/core/actors/messages/Message.java         |  21 +
 .../saga/core/actors/messages/TransactMessage.java |  39 ++
 .../messages/TransactionRecoveryMessage.java       |  32 +
 .../core/application/SagaExecutionComponent.java   |  87 +++
 .../saga/core/application/SagaFactory.java         |  30 +
 .../application/interpreter/FromJsonFormat.java    |  24 +
 .../interpreter/RestRequestChecker.java            |  55 ++
 .../saga/core/dag/ByLevelTraveller.java            |  83 +++
 .../saga/core/dag/FromLeafTraversalDirection.java  |  38 ++
 .../saga/core/dag/FromRootTraversalDirection.java  |  38 ++
 .../saga/core/dag/GraphBasedSagaFactory.java       |  80 +++
 .../servicecomb/saga/core/dag/GraphBuilder.java    | 109 ++++
 .../saga/core/dag/GraphCycleDetector.java          |  25 +
 .../saga/core/dag/GraphCycleDetectorImpl.java      |  70 +++
 .../org/apache/servicecomb/saga/core/dag/Node.java |  86 +++
 .../core/dag/SingleLeafDirectedAcyclicGraph.java   |  38 ++
 .../servicecomb/saga/core/dag/Traveller.java       |  29 +
 .../saga/core/dag/TraversalDirection.java          |  29 +
 .../infrastructure/ContextAwareEventStore.java     |  56 ++
 .../saga/infrastructure/EmbeddedEventStore.java    |  58 ++
 .../servicecomb/saga/transports/RestTransport.java |  27 +
 .../saga/transports/TransportFactory.java          |  23 +
 .../saga/core/BackwardRecoveryTest.java            |  57 ++
 .../servicecomb/saga/core/CompensationImpl.java    |  27 +
 .../saga/core/CompositeSagaLogTest.java            |  49 ++
 .../saga/core/CompositeSagaResponseTest.java       |  73 +++
 .../apache/servicecomb/saga/core/DummyEvent.java   |  31 +
 .../servicecomb/saga/core/FallbackPolicyTest.java  |  91 +++
 .../servicecomb/saga/core/ForwardRecoveryTest.java |  59 ++
 .../servicecomb/saga/core/LongIdGeneratorTest.java |  36 ++
 .../servicecomb/saga/core/RestOperationTest.java   |  89 +++
 .../servicecomb/saga/core/RetrySagaLogTest.java    |  80 +++
 .../servicecomb/saga/core/SagaEndTaskTest.java     |  47 ++
 .../servicecomb/saga/core/SagaEventMatcher.java    |  68 ++
 .../saga/core/SagaExecutionComponentTestBase.java  | 250 ++++++++
 .../servicecomb/saga/core/SagaIntegrationTest.java | 687 ++++++++++++++++++++
 .../servicecomb/saga/core/SagaStartTaskTest.java   |  64 ++
 .../servicecomb/saga/core/TransactionImpl.java     |  27 +
 .../ActorBasedSagaExecutionComponentTest.java      |  31 +
 .../core/actors/ActorBasedSagaIntegrationTest.java | 691 +++++++++++++++++++++
 .../core/actors/CompletionCallbackActorTest.java   | 138 ++++
 .../saga/core/actors/EventContextImplTest.java     |  93 +++
 .../saga/core/actors/RequestActorBuilderTest.java  | 173 ++++++
 .../saga/core/actors/RequestActorTest.java         | 399 ++++++++++++
 .../dag/DirectedAcyclicGraphTraversalTest.java     | 102 +++
 .../dag/GraphBasedSagaExecutionComponentTest.java  |  33 +
 .../saga/core/dag/GraphBuilderTest.java            | 148 +++++
 .../saga/core/dag/GraphCycleDetectorTest.java      |  73 +++
 .../apache/servicecomb/saga/core/dag/NodeTest.java |  80 +++
 .../infrastructure/ContextAwareEventStoreTest.java |  43 ++
 saga-core/src/test/resources/application.conf      |  21 +
 saga-core/src/test/resources/log4j2.xml            |  30 +
 saga-demo/conditional-transaction-demo/README.md   | 174 ++++++
 .../conditional-transaction-demo-tests/pom.xml     | 301 +++++++++
 .../saga/demo/tests/ConditionalTransactionIT.java  | 155 +++++
 .../docker-compose.yaml                            |  75 +++
 .../conditional-transaction-demo/inventory/pom.xml |  74 +++
 .../inventory/InventoryApplication.java            |  29 +
 .../transaction/inventory/InventoryController.java |  89 +++
 .../inventory/InventoryControllerTest.java         |  79 +++
 .../membership/pom.xml                             |  74 +++
 .../membership/MembershipApplication.java          |  29 +
 .../membership/MembershipController.java           |  62 ++
 .../conditional-transaction-demo/payment/pom.xml   |  75 +++
 .../transaction/payment/PaymentApplication.java    |  29 +
 .../transaction/payment/PaymentController.java     |  84 +++
 .../transaction/payment/PaymentControllerTest.java |  61 ++
 saga-demo/conditional-transaction-demo/pom.xml     |  40 ++
 .../conditional-transaction-demo/supplier/pom.xml  |  74 +++
 .../transaction/supplier/SupplierApplication.java  |  29 +
 .../transaction/supplier/SupplierController.java   |  52 ++
 .../src/main/resources/META-INF/LICENSE.txt        | 202 ++++++
 .../src/main/resources/META-INF/NOTICE.txt         |  11 +
 .../dependency-free-transaction-demo/README.md     | 168 +++++
 .../car-rental-service/pom.xml                     |  85 +++
 .../saga/demo/car/rental/CarRentalApplication.java |  32 +
 .../saga/demo/car/rental/CarRentalController.java  | 100 +++
 .../src/main/resources/microservice.yaml           |  30 +
 .../demo-tests/pom.xml                             | 333 ++++++++++
 .../apache/servicecomb/saga/demo/tests/DemoIT.java | 150 +++++
 .../docker-compose.yaml                            | 101 +++
 .../flight-booking-service/pom.xml                 |  85 +++
 .../flight/booking/FlightBookingApplication.java   |  32 +
 .../flight/booking/FlightBookingController.java    |  77 +++
 .../src/main/resources/microservice.yaml           |  30 +
 .../hotel-reservation-service/pom.xml              |  85 +++
 .../reservation/HotelReservationApplication.java   |  32 +
 .../reservation/HotelReservationController.java    |  78 +++
 .../src/main/resources/microservice.yaml           |  30 +
 .../payment-service/pom.xml                        |  85 +++
 .../saga/demo/payment/PaymentApplication.java      |  32 +
 .../saga/demo/payment/PaymentController.java       |  81 +++
 .../src/main/resources/microservice.yaml           |  32 +
 saga-demo/dependency-free-transaction-demo/pom.xml |  67 ++
 saga-demo/pom.xml                                  |   2 +
 .../saga/demo/pack/booking/BookingController.java  |   1 +
 saga-discovery/pom.xml                             |  37 ++
 .../saga-discovery-servicecenter/pom.xml           | 124 ++++
 .../center/ServiceCenterDiscoveryConfig.java       |  43 ++
 .../src/main/resources/META-INF/spring.factories   |  18 +
 .../src/main/resources/microservice.yaml           |  30 +
 .../discovery/service/center/DummyController.java  |  56 ++
 .../center/ServiceCenterDiscoveryApplication.java  |  32 +
 .../ServiceCenterDiscoveryRestTransportTest.java   | 132 ++++
 .../src/test/resources/log4j2-test.xml             |  30 +
 .../src/test/resources/registry.yaml               |  23 +
 saga-format/pom.xml                                |  74 +++
 .../saga/core/FailedSagaRequestContext.java        |  40 ++
 .../servicecomb/saga/core/JacksonToJsonFormat.java |  65 ++
 .../servicecomb/saga/core/SagaRequestContext.java  |  37 ++
 .../saga/core/SuccessfulSagaRequestContext.java    |  41 ++
 .../servicecomb/saga/format/ChildrenExtractor.java |  55 ++
 .../servicecomb/saga/format/JacksonFallback.java   |  64 ++
 .../saga/format/JacksonFromJsonFormat.java         |  58 ++
 .../saga/format/JacksonRestCompensation.java       |  52 ++
 .../saga/format/JacksonRestFallback.java           |  43 ++
 .../saga/format/JacksonRestOperation.java          |  62 ++
 .../saga/format/JacksonRestTransaction.java        |  36 ++
 .../saga/format/JacksonSagaEventFormat.java        | 114 ++++
 .../saga/format/JsonFailedSagaResponse.java        |  29 +
 .../saga/format/JsonRestSagaRequest.java           |  72 +++
 .../saga/format/JsonSagaDefinition.java            |  60 ++
 .../servicecomb/saga/format/JsonSagaRequest.java   |  39 ++
 .../saga/format/JsonSuccessfulSagaResponse.java    |  30 +
 .../servicecomb/saga/format/SagaEventFormat.java   |  25 +
 .../servicecomb/saga/format/TransportAware.java    |  26 +
 .../saga/format/ChildrenExtractorTest.java         |  75 +++
 .../saga/format/JacksonFromJsonFormatTest.java     | 261 ++++++++
 .../saga/format/JacksonRestOperationTest.java      |  69 ++
 .../saga/format/JsonRestSagaRequestTest.java       |  95 +++
 .../saga/format/JsonSagaDefinitionTest.java        |  34 +
 .../saga/format/SagaEventFormatTest.java           | 180 ++++++
 saga-performance/README.md                         |  47 ++
 saga-performance/images/grafana.png                | Bin 0 -> 15508 bytes
 saga-performance/scripts/saga.jmx                  | 289 +++++++++
 saga-spring/pom.xml                                | 233 +++++++
 .../saga/spring/JpaPersistentStore.java            |  69 ++
 .../servicecomb/saga/spring/SagaController.java    | 222 +++++++
 .../servicecomb/saga/spring/SagaEventEntity.java   |  85 +++
 .../servicecomb/saga/spring/SagaEventRepo.java     |  44 ++
 .../saga/spring/SagaExecutionQueryService.java     | 160 +++++
 .../saga/spring/SagaRecoveryListener.java          |  37 ++
 .../saga/spring/SagaShutdownListener.java          |  42 ++
 .../saga/spring/SagaSpringApplication.java         |  39 ++
 .../servicecomb/saga/spring/SagaSpringConfig.java  | 136 ++++
 saga-spring/src/main/resources/META-INF/aop.xml    |  25 +
 .../src/main/resources/META-INF/spring.factories   |  20 +
 saga-spring/src/main/resources/application.conf    |  88 +++
 saga-spring/src/main/resources/application.yaml    |  30 +
 saga-spring/src/main/resources/log4j2.xml          |  30 +
 .../src/main/resources/schema-postgresql.sql       |  26 +
 .../ActorBasedSagaSpringApplicationTest.java       |  25 +
 .../GraphBasedSagaSpringApplicationTest.java       |  25 +
 .../saga/spring/GreetingController.java            |  42 ++
 .../servicecomb/saga/spring/SagaRecoveryTest.java  | 181 ++++++
 .../saga/spring/SagaServiceDiscoveryTest.java      | 113 ++++
 .../saga/spring/SagaSpringApplicationTestBase.java | 390 ++++++++++++
 saga-spring/src/test/resources/data.sql            |  29 +
 saga-spring/src/test/resources/log4j2-test.xml     |  30 +
 saga-spring/src/test/resources/registry.yaml       |  23 +
 saga-web/pom.xml                                   | 130 ++++
 .../servicecomb/saga/web/SagaWebApplication.java   |  34 +
 saga-web/src/main/resources/application.yaml       |  29 +
 saga-web/src/main/resources/microservice.yaml      |  31 +
 saga-web/src/main/resources/static/css/request.css |  66 ++
 saga-web/src/main/resources/static/css/style.css   |  41 ++
 saga-web/src/main/resources/static/detail.html     | 174 ++++++
 saga-web/src/main/resources/static/index.html      |  31 +
 saga-web/src/main/resources/static/js/date.js      | 660 ++++++++++++++++++++
 saga-web/src/main/resources/static/js/request.js   | 294 +++++++++
 saga-web/src/main/resources/static/js/table.js     | 128 ++++
 saga-web/src/main/resources/static/request.html    | 232 +++++++
 saga-web/src/main/resources/static/result.html     |  67 ++
 transports/pom.xml                                 |  39 ++
 transports/transport-httpclient-spring/pom.xml     |  80 +++
 .../saga/transports/HttpClientTransportConfig.java |  40 ++
 .../src/main/resources/META-INF/spring.factories   |  19 +
 .../saga/transports/RestTransportTest.java         | 123 ++++
 .../src/test/resources/log4j2.xml                  |  30 +
 transports/transport-httpclient/pom.xml            |  63 ++
 .../transports/httpclient/HttpClientTransport.java | 121 ++++
 .../httpclient/HttpClientTransportTest.java        | 192 ++++++
 .../src/test/resources/log4j2.xml                  |  30 +
 transports/transport-resttemplate/pom.xml          |  71 +++
 .../resttemplate/RestTemplateTransport.java        | 121 ++++
 257 files changed, 19897 insertions(+)

diff --git a/docs/api/api.md b/docs/api/api.md
new file mode 100755
index 0000000..e0a9e8f
--- /dev/null
+++ b/docs/api/api.md
@@ -0,0 +1,168 @@
+# Saga API
+### Post transaction and compensation requests to Saga
+```
+POST /requests
+```
+
+####  Description
+
+1. Define requests in order and recovery policy by JSON format as below,put them to body.
+```
+{
+  "policy": "",
+  "requests": [
+    {
+      "id": "",
+      "type": "",
+      "serviceName": "",
+      "parents": [
+
+      ],
+      "transaction": {
+        "method": "",
+        "path": "",
+        "params": {
+
+        }
+      },
+      "compensation": {
+        "method": "",
+        "path": "",
+        "params": {
+
+        }
+      }
+    }
+  ]
+}
+```
+JSON parameters:
+- policy - support `BackwardRecovery` or `ForwardRecovery`.
+- requests - transactions array.
+  - id - request id. It should be unique among this collection of requests.
+  - type - support `rest` for now.
+  - serviceName - user-defined service name.
+  - parents - request ids. It means this request is only executed after all requests in the parents field are completed.
+  - transaction - user-defined transaction that executed by the Saga.
+    - method - user-defined, HTTP method.
+    - path - user-defined, HTTP path.
+    - params - support `form`,`json`,`body`,`query`.
+  - compensation - user-defined compensation that executed by the Saga.
+    - method - user-defined, HTTP method.
+    - path - user-defined, HTTP path.
+    - params - support `form`,`json`,`body`,`query`.
+
+2. Set content type to `text/plain`.
+
+3. Send them to Saga service.
+
+####  Example request
+```
+curl -XPOST -H "Content-Type: text/plain" -d @./request.json  http://<docker.host.ip:saga.port>/requests
+```
+
+####  Example response
+```
+success
+```
+
+####  Status codes
+-   **200** – no error
+-   **400** – bad parameter
+-   **500** – server error
+
+
+### Get all the Saga events
+```
+GET /events
+```
+
+####  Description
+Get all the Saga events.
+
+####  Example request
+```
+curl -XGET http://<docker.host.ip:saga.port>/events
+```
+
+####  Example response
+```
+{
+    "88658e73-eff5-4d31-887e-019201d6b560": [
+        {
+            "id": 1,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:40Z",
+            "type": "SagaStartedEvent",
+            "contentJson": "{\"policy\": \"BackwardRecovery\", \"requests\": [{\"id\": \"request-car\", \"type\": \"rest\", \"serviceName\": \"car-rental-service\", \"transaction\": {\"path\": \"/rentals\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/rentals\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}}, {\"id\": \"request-hotel\", \"type\": \"rest\", \"serviceName\": \"hotel-reservation-service [...]
+        },
+        {
+            "id": 2,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:40Z",
+            "type": "TransactionStartedEvent",
+            "contentJson": "{\"id\": \"request-flight\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"flight-booking-service\", \"transaction\": {\"path\": \"/bookings\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/bookings\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+        },
+        {
+            "id": 3,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:40Z",
+            "type": "TransactionStartedEvent",
+            "contentJson": "{\"id\": \"request-car\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"car-rental-service\", \"transaction\": {\"path\": \"/rentals\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/rentals\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+        },
+        {
+            "id": 4,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:40Z",
+            "type": "TransactionStartedEvent",
+            "contentJson": "{\"id\": \"request-hotel\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"hotel-reservation-service\", \"transaction\": {\"path\": \"/reservations\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/reservations\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+        },
+        {
+            "id": 5,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:40Z",
+            "type": "TransactionEndedEvent",
+            "contentJson": "{\"request\": {\"id\": \"request-flight\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"flight-booking-service\", \"transaction\": {\"path\": \"/bookings\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/bookings\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"response\": {\"body\": \"{\\n  \\\"statusCode\\\" [...]
+        },
+        {
+            "id": 6,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:40Z",
+            "type": "TransactionEndedEvent",
+            "contentJson": "{\"request\": {\"id\": \"request-hotel\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"hotel-reservation-service\", \"transaction\": {\"path\": \"/reservations\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/reservations\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"response\": {\"body\": \"{\\n  \\\"stat [...]
+        },
+        {
+            "id": 7,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:41Z",
+            "type": "TransactionEndedEvent",
+            "contentJson": "{\"request\": {\"id\": \"request-car\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"car-rental-service\", \"transaction\": {\"path\": \"/rentals\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/rentals\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"response\": {\"body\": \"{\\n  \\\"statusCode\\\": 200,\\n [...]
+        },
+        {
+            "id": 8,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:41Z",
+            "type": "TransactionStartedEvent",
+            "contentJson": "{\"id\": \"request-payment\", \"type\": \"rest\", \"parents\": [\"request-car\", \"request-flight\", \"request-hotel\"], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"payment-service\", \"transaction\": {\"path\": \"/payments\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/payments\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+        },
+        {
+            "id": 9,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:41Z",
+            "type": "TransactionEndedEvent",
+            "contentJson": "{\"request\": {\"id\": \"request-payment\", \"type\": \"rest\", \"parents\": [\"request-car\", \"request-flight\", \"request-hotel\"], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"payment-service\", \"transaction\": {\"path\": \"/payments\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/payments\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"r [...]
+        },
+        {
+            "id": 10,
+            "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+            "creationTime": "2017-09-15T01:15:41Z",
+            "type": "SagaEndedEvent",
+            "contentJson": "{}"
+        }
+    ]
+}
+```
+
+####  Status codes
+-   **200** – no error
+
diff --git a/docs/old_saga.md b/docs/old_saga.md
new file mode 100755
index 0000000..2b79d22
--- /dev/null
+++ b/docs/old_saga.md
@@ -0,0 +1,53 @@
+# Previous Saga's Documentation
+## Major Architecture of Saga
+* saga-core(transaction and compensation handling logic)
+* saga-format(data serialization and deserialization)
+* saga-transports(communication protocol implementation such as rest or rpc in the future)
+* saga-discovery(service discovery)
+* saga-spring(restful service framework)
+
+![Saga](static_files/saga.png) 
+
+## Prerequisites
+You will need:
+1. [Oracle JDK 1.8+][jdk]
+2. [Maven 3.x][maven]
+3. [Docker][docker]
+4. [PostgreSQL][postgres]
+5. [Service Center(optional)][service_center]
+6. [Docker compose(optional)][docker_compose]
+7. [Docker machine(optional)][docker_machine]
+
+[jdk]: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
+[maven]: https://maven.apache.org/install.html
+[docker]: https://www.docker.com/get-docker
+[postgres]: https://www.postgresql.org/download/
+[service_center]: https://github.com/apache/incubator-servicecomb-service-center
+[docker_compose]: https://docs.docker.com/compose/install/
+[docker_machine]: https://docs.docker.com/machine/install-machine/
+
+## Building
+Download the source code.
+```
+git clone https://github.com/apache/incubator-servicecomb-saga.git
+```
+
+Enter the Saga root directory,build Saga project by maven command and generate a docker image named saga-spring in local.
+```
+mvn package -DskipTests -Pdocker
+```
+
+## Run Services
+A `docker-compose.yaml` file is provided to start Saga services and its dependencies(Service center and Mysql) as docker containers.
+User also can configure specified Service center or Mysql in `docker-compose.yaml`.
+
+Enter the Saga root directory, run all service images using command,
+```
+docker-compose up
+```
+
+## Reference API
+See [Saga API](docs/api/api.md) for details.
+
+## Example
+See [Saga demo](https://github.com/apache/incubator-servicecomb-saga/tree/master/saga-demo) for details.
diff --git a/integration-tests/coverage-aggregate/pom.xml b/integration-tests/coverage-aggregate/pom.xml
index ccd5b9a..ef4df2e 100644
--- a/integration-tests/coverage-aggregate/pom.xml
+++ b/integration-tests/coverage-aggregate/pom.xml
@@ -31,6 +31,26 @@
   <dependencies>
     <dependency>
       <groupId>org.apache.servicecomb.saga</groupId>
+      <artifactId>saga-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.saga</groupId>
+      <artifactId>saga-format</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.saga</groupId>
+      <artifactId>saga-spring</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.saga.transports</groupId>
+      <artifactId>transport-httpclient</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.saga.discovery</groupId>
+      <artifactId>saga-discovery-servicecenter</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.saga</groupId>
       <artifactId>omega-context</artifactId>
     </dependency>
     <dependency>
diff --git a/pom.xml b/pom.xml
index 8f91a95..99a0fe2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,13 @@
   <version>0.3.0-SNAPSHOT</version>
 
   <modules>
+    <module>saga-core</module>
+    <module>transports</module>
+    <module>saga-spring</module>
     <module>docker-build-config</module>
+    <module>saga-format</module>
+    <module>saga-discovery</module>
+    <module>saga-web</module>
     <module>omega</module>
     <module>alpha</module>
     <module>pack-contracts</module>
@@ -124,6 +130,41 @@
     <dependencies>
       <dependency>
         <groupId>org.apache.servicecomb.saga</groupId>
+        <artifactId>saga-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga</groupId>
+        <artifactId>saga-format</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga</groupId>
+        <artifactId>saga-spring</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga.transports</groupId>
+        <artifactId>transport-httpclient</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga.transports</groupId>
+        <artifactId>transport-httpclient-spring</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga.transports</groupId>
+        <artifactId>transport-resttemplate</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga.discovery</groupId>
+        <artifactId>saga-discovery-servicecenter</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.servicecomb.saga</groupId>
         <artifactId>omega-context</artifactId>
         <version>0.3.0-SNAPSHOT</version>
       </dependency>
diff --git a/saga-core/pom.xml b/saga-core/pom.xml
new file mode 100644
index 0000000..1cc1c61
--- /dev/null
+++ b/saga-core/pom.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <artifactId>saga</artifactId>
+    <groupId>org.apache.servicecomb.saga</groupId>
+    <version>0.3.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>saga-core</artifactId>
+  <name>Saga::Core</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.lmax</groupId>
+      <artifactId>disruptor</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.typesafe.akka</groupId>
+      <artifactId>akka-actor_2.12</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.typesafe.akka</groupId>
+      <artifactId>akka-slf4j_2.12</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.kamon</groupId>
+      <artifactId>kamon-core_2.12</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.kamon</groupId>
+      <artifactId>kamon-annotation_2.12</artifactId>
+    </dependency>
+
+    <!-- test dependencies -->
+    <dependency>
+      <groupId>com.typesafe.akka</groupId>
+      <artifactId>akka-testkit_2.12</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.scalatest</groupId>
+      <artifactId>scalatest_2.12</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-all</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.github.seanyinx</groupId>
+      <artifactId>unit-scaffolding</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/BackwardRecovery.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/BackwardRecovery.java
new file mode 100644
index 0000000..e46de2f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/BackwardRecovery.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+@EnableKamon
+public class BackwardRecovery implements RecoveryPolicy {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @Segment(name = "backwardPolicy", category = "application", library = "kamon")
+  @Override
+  public SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse) {
+    try {
+      return request.transaction().send(request.serviceName(), parentResponse);
+    } catch (Exception e) {
+      log.error("Applying {} policy due to failure in transaction {} of service {}",
+          description(),
+          request.transaction(),
+          request.serviceName(),
+          e
+      );
+
+      task.abort(request, e);
+      throw e;
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Compensation.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Compensation.java
new file mode 100644
index 0000000..f642011
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Compensation.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface Compensation extends Operation {
+
+  Compensation SAGA_START_COMPENSATION = new Compensation() {
+  };
+
+  Compensation SAGA_END_COMPENSATION = new Compensation() {
+  };
+
+  int DEFAULT_RETRIES = 3;
+
+  default int retries() {
+    return DEFAULT_RETRIES;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompensationTaskConsumer.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompensationTaskConsumer.java
new file mode 100644
index 0000000..736c34d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompensationTaskConsumer.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CompensationTaskConsumer implements TaskConsumer {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private final Map<String, SagaTask> tasks;
+  private final SagaContext sagaContext;
+
+  CompensationTaskConsumer(Map<String, SagaTask> tasks, SagaContext sagaContext) {
+    this.tasks = tasks;
+    this.sagaContext = sagaContext;
+  }
+
+  @Override
+  public void consume(Collection<Node<SagaRequest>> nodes) {
+    for (Node<SagaRequest> node : nodes) {
+      SagaRequest request = node.value();
+
+      if (sagaContext.isTransactionCompleted(request)) {
+        log.info("Starting request {} id={}", request.serviceName(), request.id());
+        tasks.get(request.task()).compensate(request);
+        log.info("Completed request {} id={}", request.serviceName(), request.id());
+      }
+    }
+  }
+
+  @Override
+  public boolean replay(Collection<Node<SagaRequest>> nodes) {
+
+    for (Iterator<Node<SagaRequest>> iterator = nodes.iterator(); iterator.hasNext(); ) {
+      SagaRequest request = iterator.next().value();
+      if (sagaContext.isCompensationCompleted(request)) {
+        log.info("Skipped completed compensation id={} operation={} while replay", request.id(), request.transaction());
+        iterator.remove();
+      } else if (!sagaContext.isTransactionCompleted(request)) {
+        // this transaction never started
+        log.info("Skipped pending transaction id={} operation={} while replay", request.id(), request.transaction());
+        iterator.remove();
+      }
+    }
+    return !nodes.isEmpty();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaLog.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaLog.java
new file mode 100644
index 0000000..017129a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaLog.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+class CompositeSagaLog implements SagaLog {
+
+  private final SagaLog embedded;
+  private final PersistentLog persistent;
+
+  CompositeSagaLog(SagaLog embedded, PersistentLog persistent) {
+    this.embedded = embedded;
+    this.persistent = persistent;
+  }
+
+  @Segment(name = "compositeSagaLog", category = "application", library = "kamon")
+  @Override
+  public void offer(SagaEvent sagaEvent) {
+    persistent.offer(sagaEvent);
+    embedded.offer(sagaEvent);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaResponse.java
new file mode 100644
index 0000000..02ab48c
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaResponse.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.Collection;
+import java.util.Optional;
+
+public class CompositeSagaResponse implements SagaResponse {
+  private final Collection<SagaResponse> responses;
+
+  public CompositeSagaResponse(Collection<SagaResponse> responses) {
+    this.responses = responses;
+  }
+
+  @Override
+  public boolean succeeded() {
+    return responses.stream().allMatch(SagaResponse::succeeded);
+  }
+
+  @Override
+  public String body() {
+    Optional<String> reduce = responses.stream()
+        .map(SagaResponse::body)
+        .reduce((a, b) -> a + ", " + b)
+        .map(combined -> "[" + combined + "]");
+
+    return reduce.orElse("{}");
+  }
+
+  public Collection<SagaResponse> responses() {
+    return responses;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Descriptive.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Descriptive.java
new file mode 100644
index 0000000..bd367af
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Descriptive.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+interface Descriptive {
+
+  default String description() {
+    return getClass().getSimpleName();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventContext.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventContext.java
new file mode 100644
index 0000000..79b0c3e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventContext.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface EventContext {
+  void beginTransaction(SagaRequest request);
+
+  void endTransaction(SagaRequest request, SagaResponse response);
+
+  void abortTransaction(SagaRequest request, SagaResponse response);
+
+  void compensateTransaction(SagaRequest request, SagaResponse response);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventEnvelope.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventEnvelope.java
new file mode 100644
index 0000000..9283b33
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventEnvelope.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class EventEnvelope {
+
+  public final long id;
+  public final long timestamp;
+  public final SagaEvent event;
+
+  public EventEnvelope(long id, SagaEvent event) {
+    this.id = id;
+    this.event = event;
+    this.timestamp = System.currentTimeMillis();
+  }
+
+  // TODO: 8/21/2017 this class seems to be useless to saga
+  public EventEnvelope(long id, long timestamp, SagaEvent event) {
+    this.id = id;
+    this.timestamp = timestamp;
+    this.event = event;
+  }
+
+  @Override
+  public String toString() {
+    return "EventEnvelope{" +
+        "id=" + id +
+        ", timestamp=" + timestamp +
+        ", event={" + event +
+        "}}";
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventStore.java
new file mode 100644
index 0000000..7b7e6e2
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventStore.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface EventStore extends SagaLog, Iterable<SagaEvent> {
+
+  void populate(Iterable<EventEnvelope> events);
+
+  long size();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/FailedSagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FailedSagaResponse.java
new file mode 100644
index 0000000..15d1701
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FailedSagaResponse.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class FailedSagaResponse implements SagaResponse {
+
+  private final String body;
+
+  public FailedSagaResponse(String body) {
+    this.body = body;
+  }
+
+  public FailedSagaResponse(Throwable e) {
+    this.body = stackTrace(e);
+  }
+
+  @Override
+  public boolean succeeded() {
+    return false;
+  }
+
+  @Override
+  public String body() {
+    return body;
+  }
+
+  private String stackTrace(Throwable e) {
+    StringWriter writer = new StringWriter();
+    e.printStackTrace(new PrintWriter(writer));
+    return writer.toString();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Fallback.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Fallback.java
new file mode 100644
index 0000000..fca68a0
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Fallback.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface Fallback extends Operation {
+
+  Fallback NOP_FALLBACK = () -> TYPE_NOP;
+
+  String type();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/FallbackPolicy.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FallbackPolicy.java
new file mode 100644
index 0000000..48ad67d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FallbackPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FallbackPolicy {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final int retryDelay;
+
+  public FallbackPolicy(int retryDelay) {
+    this.retryDelay = retryDelay;
+  }
+
+  public SagaResponse apply(String address, Compensation compensation, Fallback fallback) {
+    for (int i = 0; isRetryable(i, compensation) && !isInterrupted(); i++) {
+      try {
+        return compensation.send(address);
+      } catch (Exception e) {
+        log.error("Failed to send compensation to {}", address, e);
+        sleep(retryDelay);
+      }
+    }
+
+    log.warn("Falling back after {} failures sending compensation to {}", compensation.retries(), address);
+    return fallback.send(address);
+  }
+
+  private boolean isRetryable(int i, Compensation compensation) {
+    return compensation.retries() < 0 || i <= compensation.retries();
+  }
+
+  private boolean isInterrupted() {
+    return Thread.currentThread().isInterrupted();
+  }
+
+  private void sleep(int delay) {
+    try {
+      Thread.sleep(delay);
+    } catch (InterruptedException ignored) {
+      Thread.currentThread().interrupt();
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/ForwardRecovery.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ForwardRecovery.java
new file mode 100644
index 0000000..c783990
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ForwardRecovery.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ForwardRecovery implements RecoveryPolicy {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  @Override
+  public SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse) {
+    try {
+      do {
+        try {
+          return request.transaction().send(request.serviceName(), parentResponse);
+        } catch (Exception e) {
+          log.error("Applying {} policy due to failure in transaction {} of service {}",
+              description(),
+              request.transaction(),
+              request.serviceName(),
+              e
+          );
+          Thread.sleep(request.failRetryDelayMilliseconds());
+        }
+      } while (true);
+    } catch (InterruptedException ignored) {
+      log.warn("Applying {} interrupted in transaction {} of service {}",
+          description(),
+          request.transaction(),
+          request.serviceName(),
+          ignored);
+      throw new TransactionFailedException(ignored);
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/GraphBasedSaga.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/GraphBasedSaga.java
new file mode 100644
index 0000000..f4544e3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/GraphBasedSaga.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.Executors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.servicecomb.saga.core.dag.ByLevelTraveller;
+import org.apache.servicecomb.saga.core.dag.FromLeafTraversalDirection;
+import org.apache.servicecomb.saga.core.dag.FromRootTraversalDirection;
+import org.apache.servicecomb.saga.core.dag.SingleLeafDirectedAcyclicGraph;
+import org.apache.servicecomb.saga.core.dag.TraversalDirection;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class GraphBasedSaga implements Saga {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final EventStore eventStore;
+  private final Map<String, SagaTask> tasks;
+
+  private final TaskRunner transactionTaskRunner;
+  private final TaskRunner compensationTaskRunner;
+  private final SagaContext sagaContext;
+  private volatile SagaState currentTaskRunner;
+
+  GraphBasedSaga(EventStore eventStore,
+      Map<String, SagaTask> tasks,
+      SagaContext sagaContext,
+      SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph) {
+
+    this(eventStore, Executors.newFixedThreadPool(5), tasks, sagaContext, sagaTaskGraph);
+  }
+
+  public GraphBasedSaga(EventStore eventStore,
+      Executor executor,
+      Map<String, SagaTask> tasks,
+      SagaContext sagaContext,
+      SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph) {
+
+    this.eventStore = eventStore;
+    this.tasks = tasks;
+
+    this.transactionTaskRunner = new TaskRunner(
+        traveller(sagaTaskGraph, new FromRootTraversalDirection<>()),
+        new TransactionTaskConsumer(
+            tasks,
+            sagaContext,
+            new ExecutorCompletionService<>(executor)));
+
+    this.sagaContext = sagaContext;
+    this.compensationTaskRunner = new TaskRunner(
+        traveller(sagaTaskGraph, new FromLeafTraversalDirection<>()),
+        new CompensationTaskConsumer(tasks, sagaContext));
+
+    currentTaskRunner = transactionTaskRunner;
+  }
+
+  @Override
+  @Segment(name = "runSaga", category = "application", library = "kamon")
+  public SagaResponse run() {
+    SagaResponse response = SagaResponse.EMPTY_RESPONSE;
+    log.info("Starting Saga");
+    do {
+      try {
+        currentTaskRunner.run();
+      } catch (TransactionFailedException e) {
+        response = new FailedSagaResponse(e);
+        log.error("Failed to run operation", e);
+        currentTaskRunner = compensationTaskRunner;
+
+        sagaContext.handleHangingTransactions(request -> {
+          tasks.get(request.task()).commit(request, sagaContext.responseOf(request.parents()));
+          tasks.get(request.task()).compensate(request);
+        });
+      }
+    } while (currentTaskRunner.hasNext());
+    log.info("Completed Saga");
+    return response;
+  }
+
+  @Override
+  public void play() {
+    log.info("Start playing events");
+    gatherEvents(eventStore);
+
+    transactionTaskRunner.replay();
+
+    if (sagaContext.isCompensationStarted()) {
+      currentTaskRunner = compensationTaskRunner;
+      compensationTaskRunner.replay();
+    }
+
+    log.info("Completed playing events");
+  }
+
+  private void gatherEvents(Iterable<SagaEvent> events) {
+    for (SagaEvent event : events) {
+      event.gatherTo(sagaContext);
+    }
+  }
+
+  private ByLevelTraveller<SagaRequest> traveller(
+      SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph,
+      TraversalDirection<SagaRequest> traversalDirection) {
+
+    return new ByLevelTraveller<>(sagaTaskGraph, traversalDirection);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/IdGenerator.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/IdGenerator.java
new file mode 100644
index 0000000..2add260
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/IdGenerator.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.io.Serializable;
+
+public interface IdGenerator<T extends Serializable> {
+
+  T nextId();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/LoggingRecoveryPolicy.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LoggingRecoveryPolicy.java
new file mode 100644
index 0000000..9173eca
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LoggingRecoveryPolicy.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+
+@EnableKamon
+public class LoggingRecoveryPolicy implements RecoveryPolicy {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final RecoveryPolicy recoveryPolicy;
+
+  public LoggingRecoveryPolicy(RecoveryPolicy recoveryPolicy) {
+    this.recoveryPolicy = recoveryPolicy;
+  }
+
+  @Segment(name = "loggingPolicy", category = "application", library = "kamon")
+  @Override
+  public SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse) {
+    log.info("Starting request id={} for service {}", request.id(), request.serviceName());
+    SagaResponse response = recoveryPolicy.apply(task, request, parentResponse);
+    log.info("Completed request id={} for service {}", request.id(), request.serviceName());
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/LongIdGenerator.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LongIdGenerator.java
new file mode 100644
index 0000000..6b16075
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LongIdGenerator.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+public class LongIdGenerator implements IdGenerator<Long> {
+
+  private final AtomicLong atomicLong = new AtomicLong();
+
+  @Override
+  public Long nextId() {
+    return atomicLong.incrementAndGet();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/NoOpSagaRequest.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/NoOpSagaRequest.java
new file mode 100644
index 0000000..56ad05e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/NoOpSagaRequest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.apache.servicecomb.saga.core.Compensation.SAGA_END_COMPENSATION;
+import static org.apache.servicecomb.saga.core.Compensation.SAGA_START_COMPENSATION;
+import static org.apache.servicecomb.saga.core.Operation.TYPE_NOP;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_END_TASK;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_START_TASK;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_END_TRANSACTION;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_START_TRANSACTION;
+
+import java.util.Arrays;
+
+public class NoOpSagaRequest implements SagaRequest {
+
+  public static final SagaRequest SAGA_START_REQUEST = new NoOpSagaRequest(
+      "saga-start",
+      SAGA_START_TRANSACTION,
+      SAGA_START_COMPENSATION,
+      SAGA_START_TASK);
+
+  public static final SagaRequest SAGA_END_REQUEST = new NoOpSagaRequest(
+      "saga-end",
+      SAGA_END_TRANSACTION,
+      SAGA_END_COMPENSATION,
+      SAGA_END_TASK);
+
+  private final String id;
+  private final Transaction transaction;
+  private final Compensation compensation;
+  private final String task;
+  private final String[] parents = {};
+
+  private NoOpSagaRequest(String id, Transaction transaction, Compensation compensation, String task) {
+    this.id = id;
+    this.transaction = transaction;
+    this.compensation = compensation;
+    this.task = task;
+  }
+
+  @Override
+  public Transaction transaction() {
+    return transaction;
+  }
+
+  @Override
+  public Compensation compensation() {
+    return compensation;
+  }
+
+  @Override
+  public String serviceName() {
+    return "Saga";
+  }
+
+  @Override
+  public String id() {
+    return id;
+  }
+
+  @Override
+  public String type() {
+    return TYPE_NOP;
+  }
+
+  @Override
+  public String task() {
+    return task;
+  }
+
+  @Override
+  public String[] parents() {
+    return parents;
+  }
+
+  @Override
+  public int failRetryDelayMilliseconds() {
+    return 50;
+  }
+
+  @Override
+  public String toString() {
+    return "NoOpSagaRequest{" +
+        "id='" + id + '\'' +
+        ", transaction=" + transaction +
+        ", compensation=" + compensation +
+        ", task='" + task + '\'' +
+        ", parents=" + Arrays.toString(parents) +
+        '}';
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Operation.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Operation.java
new file mode 100644
index 0000000..13b6f8a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Operation.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface Operation {
+
+  String TYPE_NOP = "NOP";
+  String TYPE_REST = "rest";
+  SagaResponse SUCCESSFUL_SAGA_RESPONSE = new SuccessfulSagaResponse("success");
+
+  default SagaResponse send(String address) {
+    return SUCCESSFUL_SAGA_RESPONSE;
+  }
+
+  default SagaResponse send(String address, SagaResponse response) {
+    return send(address);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentLog.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentLog.java
new file mode 100644
index 0000000..26ec4bc
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentLog.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface PersistentLog {
+  void offer(SagaEvent sagaEvent);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentStore.java
new file mode 100644
index 0000000..a7620b3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentStore.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.List;
+import java.util.Map;
+
+public interface PersistentStore extends PersistentLog {
+
+  Map<String, List<EventEnvelope>> findPendingSagaEvents();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/RecoveryPolicy.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RecoveryPolicy.java
new file mode 100644
index 0000000..c67d640
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RecoveryPolicy.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface RecoveryPolicy extends Descriptive {
+
+  String SAGA_FORWARD_RECOVERY_POLICY = "ForwardRecovery";
+  String SAGA_BACKWARD_RECOVERY_POLICY = "BackwardRecovery";
+
+  SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/RequestProcessTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RequestProcessTask.java
new file mode 100644
index 0000000..e19c7bf
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RequestProcessTask.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class RequestProcessTask implements SagaTask {
+
+  private final String sagaId;
+  private final SagaLog sagaLog;
+  private final RecoveryPolicy recoveryPolicy;
+  private final FallbackPolicy fallbackPolicy;
+
+  public RequestProcessTask(
+      String sagaId,
+      SagaLog sagaLog,
+      RecoveryPolicy recoveryPolicy,
+      FallbackPolicy fallbackPolicy) {
+
+    this.sagaId = sagaId;
+    this.sagaLog = sagaLog;
+    this.recoveryPolicy = recoveryPolicy;
+    this.fallbackPolicy = fallbackPolicy;
+  }
+
+  @Segment(name = "commit", category = "application", library = "kamon")
+  @Override
+  public SagaResponse commit(SagaRequest request, SagaResponse parentResponse) {
+    sagaLog.offer(new TransactionStartedEvent(sagaId, request));
+
+    SagaResponse response = recoveryPolicy.apply(this, request, parentResponse);
+
+    sagaLog.offer(new TransactionEndedEvent(sagaId, request, response));
+    return response;
+  }
+
+  @Segment(name = "compensate", category = "application", library = "kamon")
+  @Override
+  public void compensate(SagaRequest request) {
+    Compensation compensation = request.compensation();
+    SagaResponse response = fallbackPolicy.apply(request.serviceName(), compensation, request.fallback());
+
+    sagaLog.offer(new TransactionCompensatedEvent(sagaId, request, response));
+  }
+
+  @Override
+  public void abort(SagaRequest request, Exception e) {
+    sagaLog.offer(new TransactionAbortedEvent(sagaId, request, e));
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/RestOperation.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RestOperation.java
new file mode 100644
index 0000000..5cce22d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RestOperation.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static java.util.Collections.emptyMap;
+
+import org.apache.servicecomb.saga.core.application.interpreter.RestRequestChecker;
+import java.util.Map;
+
+public class RestOperation implements Operation {
+
+  private final String path;
+  private final String method;
+  private final Map<String, Map<String, String>> params;
+
+  public RestOperation(String path, String method, Map<String, Map<String, String>> params) {
+    RestRequestChecker.checkParameters(method, params);
+
+    this.path = path;
+    this.method = method;
+    this.params = params == null? emptyMap() : params;
+  }
+
+  public String path() {
+    return path;
+  }
+
+  public String method() {
+    return method;
+  }
+
+  public Map<String, Map<String, String>> params() {
+    return params;
+  }
+
+  @Override
+  public String toString() {
+    return "Operation{" +
+        "path='" + path + '\'' +
+        ", method='" + method + '\'' +
+        ", params=" + params +
+        '}';
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Saga.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Saga.java
new file mode 100644
index 0000000..44f62ea
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Saga.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import kamon.annotation.Segment;
+
+public interface Saga {
+  @Segment(name = "runSaga", category = "application", library = "kamon")
+  SagaResponse run();
+
+  void play();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContext.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContext.java
new file mode 100644
index 0000000..309be44
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContext.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.function.Consumer;
+
+public interface SagaContext extends EventContext {
+  boolean isCompensationStarted();
+
+  boolean isTransactionCompleted(SagaRequest request);
+
+  boolean isCompensationCompleted(SagaRequest request);
+
+  void handleHangingTransactions(Consumer<SagaRequest> consumer);
+
+  SagaResponse responseOf(String requestId);
+
+  boolean isChosenChild(SagaRequest request);
+
+  SagaResponse responseOf(String[] parentRequestIds);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContextImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContextImpl.java
new file mode 100644
index 0000000..610ae27
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContextImpl.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+
+public class SagaContextImpl implements SagaContext {
+
+  private final Map<String, SagaResponse> completedTransactions;
+  private final Map<String, SagaResponse> completedCompensations;
+  private final Set<String> abortedTransactions;
+  private final Map<String, SagaRequest> hangingTransactions;
+  private final FromJsonFormat<Set<String>> childrenExtractor;
+
+  public SagaContextImpl(FromJsonFormat<Set<String>> childrenExtractor) {
+    this.childrenExtractor = childrenExtractor;
+    this.completedTransactions = new ConcurrentHashMap<>();
+    this.completedCompensations = new HashMap<>();
+    this.abortedTransactions = new ConcurrentSkipListSet<>();
+    this.hangingTransactions = new ConcurrentHashMap<>();
+  }
+
+  @Override
+  public boolean isCompensationStarted() {
+    return !abortedTransactions.isEmpty() || !completedCompensations.isEmpty();
+  }
+
+  @Override
+  public boolean isTransactionCompleted(SagaRequest request) {
+    return completedTransactions.containsKey(request.id());
+  }
+
+  @Override
+  public boolean isCompensationCompleted(SagaRequest request) {
+    return completedCompensations.containsKey(request.id());
+  }
+
+  @Override
+  public void beginTransaction(SagaRequest request) {
+    hangingTransactions.put(request.id(), request);
+  }
+
+  @Override
+  public void endTransaction(SagaRequest request, SagaResponse response) {
+    completedTransactions.put(request.id(), response);
+    hangingTransactions.remove(request.id());
+  }
+
+  @Override
+  public void abortTransaction(SagaRequest request, SagaResponse response) {
+    completedTransactions.remove(request.id());
+    abortedTransactions.add(request.id());
+    hangingTransactions.remove(request.id());
+  }
+
+  @Override
+  public void compensateTransaction(SagaRequest request, SagaResponse response) {
+    completedCompensations.put(request.id(), response);
+    completedTransactions.remove(request.id());
+  }
+
+  @Override
+  public void handleHangingTransactions(Consumer<SagaRequest> consumer)  {
+    for (Iterator<SagaRequest> iterator = hangingTransactions.values().iterator(); iterator.hasNext(); ) {
+      consumer.accept(iterator.next());
+    }
+  }
+
+  @Override
+  public SagaResponse responseOf(String requestId) {
+    return completedTransactions.getOrDefault(requestId, SagaResponse.NONE_RESPONSE);
+  }
+
+  private List<SagaResponse> responsesOf(String[] parentRequestIds) {
+    return Arrays.stream(parentRequestIds)
+        .map(this::responseOf)
+        .collect(Collectors.toList());
+  }
+
+  @Override
+  public SagaResponse responseOf(String[] parentRequestIds) {
+    List<SagaResponse> responses = responsesOf(parentRequestIds);
+
+    if (responses.isEmpty()) {
+      return SagaResponse.EMPTY_RESPONSE;
+    }
+
+    if (responses.size() == 1) {
+      return responses.get(0);
+    }
+
+    return new CompositeSagaResponse(responses);
+  }
+
+  @Override
+  public boolean isChosenChild(SagaRequest request)  {
+    Set<String> chosenChildren = chosenChildrenOf(request.parents());
+    return chosenChildren.isEmpty() || chosenChildren.contains(request.id());
+  }
+
+  private Set<String> chosenChildrenOf(String[] parentRequestIds) {
+    return Arrays.stream(parentRequestIds)
+        .map(this::responseOf)
+        .flatMap(sagaResponse -> childrenExtractor.fromJson(sagaResponse.body()).stream())
+        .collect(Collectors.toSet());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaDefinition.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaDefinition.java
new file mode 100644
index 0000000..406c8df
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaDefinition.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface SagaDefinition {
+
+  RecoveryPolicy policy();
+
+  SagaRequest[] requests();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndTask.java
new file mode 100644
index 0000000..69daba7
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndTask.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class SagaEndTask implements SagaTask {
+
+  private final String sagaId;
+  private final SagaLog sagaLog;
+
+  public SagaEndTask(String sagaId, SagaLog sagaLog) {
+    this.sagaId = sagaId;
+    this.sagaLog = sagaLog;
+  }
+
+  @Segment(name = "endTaskCommit", category = "application", library = "kamon")
+  @Override
+  public SagaResponse commit(SagaRequest request, SagaResponse parentResponse) {
+    sagaLog.offer(new SagaEndedEvent(sagaId, request));
+    return SagaResponse.EMPTY_RESPONSE;
+  }
+
+  @Override
+  public void compensate(SagaRequest request) {
+  }
+
+  @Override
+  public void abort(SagaRequest request, Exception e) {
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndedEvent.java
new file mode 100644
index 0000000..c6f4f9f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndedEvent.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class SagaEndedEvent extends SagaEvent {
+
+  public SagaEndedEvent(String sagaId, SagaRequest sagaTask) {
+    super(sagaId, sagaTask);
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+    sagaContext.endTransaction(payload(), SagaResponse.EMPTY_RESPONSE);
+  }
+
+  @Override
+  public String toString() {
+    return "SagaEndedEvent{id="
+        + payload().id()
+        + ", sagaId=" + sagaId
+        + "}";
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEvent.java
new file mode 100644
index 0000000..ceb0ebe
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEvent.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public abstract class SagaEvent implements Descriptive {
+
+  public final String sagaId;
+  private final SagaRequest payload;
+
+  public SagaEvent(String sagaId, SagaRequest payload) {
+    this.sagaId = sagaId;
+    this.payload = payload;
+  }
+
+  public SagaRequest payload() {
+    return payload;
+  }
+
+  public abstract void gatherTo(EventContext sagaContext);
+
+  public String json(ToJsonFormat toJsonFormat) {
+    return "{}";
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaException.java
new file mode 100644
index 0000000..49ce989
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaException.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class SagaException extends RuntimeException {
+
+  public SagaException(String cause, Throwable e) {
+    super(cause, e);
+  }
+
+  public SagaException(String cause) {
+    super(cause);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaLog.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaLog.java
new file mode 100644
index 0000000..2e07b9f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaLog.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface SagaLog extends PersistentLog {
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequest.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequest.java
new file mode 100644
index 0000000..337abdd
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.apache.servicecomb.saga.core.Fallback.NOP_FALLBACK;
+
+public interface SagaRequest {
+
+  String PARAM_FORM = "form";
+  String PARAM_JSON = "json";
+  String PARAM_JSON_BODY = "body";
+  String PARAM_QUERY = "query";
+
+  Transaction transaction();
+
+  Compensation compensation();
+
+  default Fallback fallback() {
+    return NOP_FALLBACK;
+  }
+
+  String serviceName();
+
+  String id();
+
+  String type();
+
+  String task();
+
+  String[] parents();
+
+  int failRetryDelayMilliseconds();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequestImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequestImpl.java
new file mode 100644
index 0000000..cbb7a23
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequestImpl.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.apache.servicecomb.saga.core.Fallback.NOP_FALLBACK;
+
+import java.util.Arrays;
+
+public class SagaRequestImpl implements SagaRequest {
+
+  private final String id;
+  private final String serviceName;
+  private final String type;
+  private final Transaction transaction;
+  private final Compensation compensation;
+  private final String[] parents;
+  private final Fallback fallback;
+  private final int failRetryDelayMilliseconds;
+
+  public SagaRequestImpl(
+      String id,
+      String serviceName,
+      String type,
+      Transaction transaction,
+      Compensation compensation,
+      Fallback fallback,
+      String[] parents,
+      int failRetryDelayMilliseconds) {
+
+    this.id = id;
+    this.serviceName = serviceName;
+    this.type = type;
+    this.transaction = transaction;
+    this.compensation = compensation;
+    this.fallback = fallback;
+    this.failRetryDelayMilliseconds = failRetryDelayMilliseconds <= 0 ? 50 : failRetryDelayMilliseconds;
+    // TODO: 2017/10/21 set parent to root when null
+    this.parents = parents == null ? new String[0] : parents;
+  }
+
+  public SagaRequestImpl(
+      String id,
+      String serviceName,
+      String type,
+      Transaction transaction,
+      Compensation compensation,
+      Fallback fallback,
+      String[] parents) {
+    this(id, serviceName, type, transaction, compensation, fallback, parents, 0);
+  }
+
+  public SagaRequestImpl(
+      String id,
+      String serviceName,
+      String type,
+      Transaction transaction,
+      Compensation compensation,
+      Fallback fallback) {
+    this(id, serviceName, type, transaction, compensation, fallback, new String[0]);
+  }
+
+  public SagaRequestImpl(
+      String id,
+      String serviceName,
+      String type,
+      Transaction transaction,
+      Compensation compensation) {
+    this(id, serviceName, type, transaction, compensation, NOP_FALLBACK, new String[0]);
+  }
+
+  public SagaRequestImpl(
+      String id,
+      String serviceName,
+      String type,
+      Transaction transaction,
+      Compensation compensation,
+      String[] parents) {
+    this(id, serviceName, type, transaction, compensation, NOP_FALLBACK, parents);
+  }
+
+  @Override
+  public Transaction transaction() {
+    return transaction;
+  }
+
+  @Override
+  public Compensation compensation() {
+    return compensation;
+  }
+
+  @Override
+  public Fallback fallback() {
+    return fallback;
+  }
+
+  @Override
+  public String serviceName() {
+    return serviceName;
+  }
+
+  @Override
+  public String id() {
+    return id;
+  }
+
+  @Override
+  public String type() {
+    return type;
+  }
+
+  @Override
+  public String task() {
+    return SagaTask.SAGA_REQUEST_TASK;
+  }
+
+  @Override
+  public String[] parents() {
+    return parents;
+  }
+
+  @Override
+  public int failRetryDelayMilliseconds() {
+    return failRetryDelayMilliseconds;
+  }
+
+  @Override
+  public String toString() {
+    return "SagaRequest{" +
+        "id='" + id + '\'' +
+        ", serviceName='" + serviceName + '\'' +
+        ", type='" + type + '\'' +
+        ", transaction=" + transaction +
+        ", compensation=" + compensation +
+        ", fallback=" + fallback +
+        ", parents=" + Arrays.toString(parents) +
+        '}';
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaResponse.java
new file mode 100644
index 0000000..ecdfe4a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaResponse.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface SagaResponse {
+
+  SagaResponse EMPTY_RESPONSE = new SagaResponse() {
+    @Override
+    public boolean succeeded() {
+      return true;
+    }
+
+    @Override
+    public String body() {
+      return "{}";
+    }
+
+    @Override
+    public String toString() {
+      return "EMPTY_RESPONSE{body={}}";
+    }
+  };
+
+  SagaResponse NONE_RESPONSE = new SagaResponse() {
+    @Override
+    public boolean succeeded() {
+      return false;
+    }
+
+    @Override
+    public String body() {
+      return "{\n"
+          + "  \"sagaChildren\": [\"none\"]\n"
+          + "}";
+    }
+
+    @Override
+    public String toString() {
+      return "NONE_RESPONSE{body={\"sagaChildren\": [\"none\"]\n}}";
+    }
+  };
+
+  boolean succeeded();
+
+  String body();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartFailedException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartFailedException.java
new file mode 100644
index 0000000..0de35da
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartFailedException.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class SagaStartFailedException extends RuntimeException {
+
+  public SagaStartFailedException(String cause, Throwable e) {
+    super(cause, e);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartTask.java
new file mode 100644
index 0000000..d8a6bd1
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartTask.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class SagaStartTask implements SagaTask {
+
+  private final String sagaId;
+  private final String requestJson;
+  private final SagaLog sagaLog;
+
+  public SagaStartTask(String sagaId, String requestJson, SagaLog sagaLog) {
+    this.sagaId = sagaId;
+    this.requestJson = requestJson;
+    this.sagaLog = sagaLog;
+  }
+
+  @Segment(name = "startTaskCommit", category = "application", library = "kamon")
+  @Override
+  public SagaResponse commit(SagaRequest request, SagaResponse parentResponse) {
+    try {
+      sagaLog.offer(new SagaStartedEvent(sagaId, requestJson, request));
+    } catch (Exception e) {
+      throw new SagaStartFailedException("Failed to persist SagaStartedEvent for " + requestJson, e);
+    }
+    return SagaResponse.EMPTY_RESPONSE;
+  }
+
+  @Override
+  public void compensate(SagaRequest request) {
+    sagaLog.offer(new SagaEndedEvent(sagaId, request));
+  }
+
+  @Override
+  public void abort(SagaRequest request, Exception e) {
+
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartedEvent.java
new file mode 100644
index 0000000..0d82fee
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartedEvent.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class SagaStartedEvent extends SagaEvent {
+
+  private final String requestJson;
+
+  public SagaStartedEvent(String sagaId, String requestJson, SagaRequest request) {
+    super(sagaId, request);
+    this.requestJson = requestJson;
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+    sagaContext.endTransaction(payload(), SagaResponse.EMPTY_RESPONSE);
+  }
+
+  @Override
+  public String json(ToJsonFormat toJsonFormat) {
+    return requestJson;
+  }
+
+  @Override
+  public String toString() {
+    return "SagaStartedEvent{id="
+        + payload().id()
+        + ", sagaId=" + sagaId
+        + "}";
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaState.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaState.java
new file mode 100644
index 0000000..eae3678
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaState.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+interface SagaState {
+
+  boolean hasNext();
+
+  void run();
+
+  void replay();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTask.java
new file mode 100644
index 0000000..73efcc0
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTask.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface SagaTask {
+
+  String SAGA_START_TASK = "SagaStart";
+  String SAGA_REQUEST_TASK = "SagaRequest";
+  String SAGA_END_TASK = "SagaEnd";
+
+  SagaResponse commit(SagaRequest request, SagaResponse parentResponse);
+
+  void compensate(SagaRequest request);
+
+  void abort(SagaRequest request, Exception e);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTaskFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTaskFactory.java
new file mode 100644
index 0000000..0fd2450
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTaskFactory.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SagaTaskFactory {
+  private final FallbackPolicy fallbackPolicy;
+  private final RetrySagaLog retrySagaLog;
+  private final PersistentStore persistentStore;
+
+  public SagaTaskFactory(int retryDelay, PersistentStore persistentStore) {
+    this.persistentStore = persistentStore;
+
+    fallbackPolicy = new FallbackPolicy(retryDelay);
+    retrySagaLog = new RetrySagaLog(persistentStore, retryDelay);
+  }
+
+  public Map<String, SagaTask> sagaTasks(String sagaId,
+      String requestJson,
+      RecoveryPolicy recoveryPolicy,
+      EventStore sagaLog) {
+
+    SagaLog compositeSagaLog = compositeSagaLog(sagaLog, persistentStore);
+
+    return new HashMap<String, SagaTask>() {{
+      put(SagaTask.SAGA_START_TASK, new SagaStartTask(sagaId, requestJson, compositeSagaLog));
+
+      SagaLog retrySagaLog = compositeSagaLog(sagaLog, SagaTaskFactory.this.retrySagaLog);
+      put(SagaTask.SAGA_REQUEST_TASK,
+          new RequestProcessTask(sagaId, retrySagaLog, new LoggingRecoveryPolicy(recoveryPolicy), fallbackPolicy));
+      put(SagaTask.SAGA_END_TASK, new SagaEndTask(sagaId, retrySagaLog));
+    }};
+  }
+
+  private CompositeSagaLog compositeSagaLog(SagaLog sagaLog, PersistentLog persistentLog) {
+    return new CompositeSagaLog(sagaLog, persistentLog);
+  }
+
+  static class RetrySagaLog implements PersistentLog {
+
+    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private final PersistentStore persistentStore;
+    private final int retryDelay;
+
+    RetrySagaLog(PersistentStore persistentStore, int retryDelay) {
+      this.persistentStore = persistentStore;
+      this.retryDelay = retryDelay;
+    }
+
+    @Override
+    public void offer(SagaEvent sagaEvent) {
+      boolean success = false;
+      do {
+        try {
+          persistentStore.offer(sagaEvent);
+          success = true;
+          log.info("Persisted saga event {} successfully", sagaEvent);
+        } catch (Exception e) {
+          log.error("Failed to persist saga event {}", sagaEvent, e);
+          sleep(retryDelay);
+        }
+      } while (!success && !isInterrupted());
+    }
+
+    private boolean isInterrupted() {
+      return Thread.currentThread().isInterrupted();
+    }
+
+    private void sleep(int delay) {
+      try {
+        Thread.sleep(delay);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SuccessfulSagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SuccessfulSagaResponse.java
new file mode 100644
index 0000000..d69bdcd
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SuccessfulSagaResponse.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class SuccessfulSagaResponse implements SagaResponse {
+  private final String body;
+
+  public SuccessfulSagaResponse(String body) {
+    this.body = body;
+  }
+
+  @Override
+  public boolean succeeded() {
+    return true;
+  }
+
+  @Override
+  public String body() {
+    return body;
+  }
+
+  @Override
+  public String toString() {
+    return "SuccessfulSagaResponse{" +
+        "body='" + body + '\'' +
+        '}';
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskConsumer.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskConsumer.java
new file mode 100644
index 0000000..5ae7494
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskConsumer.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.Collection;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+
+interface TaskConsumer {
+
+  void consume(Collection<Node<SagaRequest>> nodes);
+
+  boolean replay(Collection<Node<SagaRequest>> nodes);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskRunner.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskRunner.java
new file mode 100644
index 0000000..c078001
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskRunner.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+import org.apache.servicecomb.saga.core.dag.Traveller;
+
+import java.util.Collection;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+class TaskRunner implements SagaState {
+
+  private final Traveller<SagaRequest> traveller;
+  private final TaskConsumer taskConsumer;
+
+  TaskRunner(Traveller<SagaRequest> traveller, TaskConsumer taskConsumer) {
+    this.traveller = traveller;
+    this.taskConsumer = taskConsumer;
+  }
+
+  @Override
+  public boolean hasNext() {
+    return traveller.hasNext();
+  }
+
+  @Segment(name = "runTask", category = "application", library = "kamon")
+  @Override
+  public void run() {
+    Collection<Node<SagaRequest>> nodes = traveller.nodes();
+
+    // finish pending tasks from saga log at startup
+    if (!nodes.isEmpty()) {
+      taskConsumer.consume(nodes);
+      nodes.clear();
+    }
+
+    while (traveller.hasNext()) {
+      traveller.next();
+      taskConsumer.consume(nodes);
+      nodes.clear();
+    }
+  }
+
+  @Override
+  public void replay() {
+    boolean played = false;
+    Collection<Node<SagaRequest>> nodes = traveller.nodes();
+
+    while (traveller.hasNext() && !played) {
+      traveller.next();
+      played = taskConsumer.replay(nodes);
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/ToJsonFormat.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ToJsonFormat.java
new file mode 100644
index 0000000..cd6c449
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ToJsonFormat.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface ToJsonFormat {
+
+  String toJson(SagaRequest request);
+
+  String toJson(SagaRequest request, SagaResponse response);
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transaction.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transaction.java
new file mode 100644
index 0000000..fafb845
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transaction.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface Transaction extends Operation {
+
+  Transaction SAGA_START_TRANSACTION = new Transaction() {
+  };
+
+  Transaction SAGA_END_TRANSACTION = new Transaction() {
+  };
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionAbortedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionAbortedEvent.java
new file mode 100644
index 0000000..b117c94
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionAbortedEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class TransactionAbortedEvent extends SagaEvent {
+
+  private final SagaResponse response;
+
+  public TransactionAbortedEvent(String sagaId, SagaRequest payload, Exception exception) {
+    super(sagaId, payload);
+    this.response = new FailedSagaResponse(exception);
+  }
+
+  public TransactionAbortedEvent(String sagaId, SagaRequest payload, SagaResponse response) {
+    super(sagaId, payload);
+    this.response = response;
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+    // remove from completed operations in order not to compensate it
+    sagaContext.abortTransaction(payload(), response);
+  }
+
+  @Override
+  public String json(ToJsonFormat toJsonFormat) {
+    return toJsonFormat.toJson(payload(), response);
+  }
+
+  @Override
+  public String toString() {
+    return "TransactionAbortedEvent{id="
+        + payload().id()
+        + ", operation="
+        + payload().compensation()
+        + "}";
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionCompensatedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionCompensatedEvent.java
new file mode 100644
index 0000000..d6ab298
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionCompensatedEvent.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class TransactionCompensatedEvent extends SagaEvent {
+
+  private final SagaResponse response;
+
+  public TransactionCompensatedEvent(String sagaId, SagaRequest request) {
+    this(sagaId, request, SagaResponse.EMPTY_RESPONSE);
+  }
+
+  public TransactionCompensatedEvent(String sagaId, SagaRequest request, SagaResponse response) {
+    super(sagaId, request);
+    this.response = response;
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+    sagaContext.compensateTransaction(payload(), response);
+  }
+
+  @Override
+  public String json(ToJsonFormat toJsonFormat) {
+    return toJsonFormat.toJson(payload(), response);
+  }
+
+  @Override
+  public String toString() {
+    return "TransactionCompensatedEvent{id="
+        + payload().id()
+        + ", sagaId=" + sagaId
+        + ", operation="
+        + payload().compensation()
+        + "}";
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionEndedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionEndedEvent.java
new file mode 100644
index 0000000..603c542
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionEndedEvent.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class TransactionEndedEvent extends SagaEvent {
+
+  private final SagaResponse response;
+
+  public TransactionEndedEvent(String sagaId, SagaRequest request) {
+    this(sagaId, request, SagaResponse.EMPTY_RESPONSE);
+  }
+
+  public TransactionEndedEvent(String sagaId, SagaRequest request, SagaResponse response) {
+    super(sagaId, request);
+    this.response = response;
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+    sagaContext.endTransaction(payload(), response);
+  }
+
+  @Override
+  public String toString() {
+    return "TransactionEndedEvent{id="
+        + payload().id()
+        + ", sagaId=" + sagaId
+        + ", operation="
+        + payload().transaction()
+        + "}";
+  }
+  
+  @Override
+  public String json(ToJsonFormat toJsonFormat) {
+    return toJsonFormat.toJson(payload(), response);
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionFailedException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionFailedException.java
new file mode 100644
index 0000000..b55e01a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionFailedException.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class TransactionFailedException extends RuntimeException {
+
+  public TransactionFailedException(Throwable throwable) {
+    super(throwable);
+  }
+
+  public TransactionFailedException(String cause) {
+    super(cause);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionStartedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionStartedEvent.java
new file mode 100644
index 0000000..cc35589
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionStartedEvent.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class TransactionStartedEvent extends SagaEvent {
+
+  public TransactionStartedEvent(String sagaId, SagaRequest transaction) {
+    super(sagaId, transaction);
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+    sagaContext.beginTransaction(payload());
+  }
+
+  @Override
+  public String toString() {
+    return "TransactionStartedEvent{id="
+        + payload().id()
+        + ", sagaId=" + sagaId
+        + ", operation="
+        + payload().transaction()
+        + "}";
+  }
+  
+  @Override
+  public String json(ToJsonFormat toJsonFormat) {
+    return toJsonFormat.toJson(payload());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionTaskConsumer.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionTaskConsumer.java
new file mode 100644
index 0000000..af2dc74
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionTaskConsumer.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+import kamon.annotation.Trace;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@EnableKamon
+class TransactionTaskConsumer implements TaskConsumer {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private final Map<String, SagaTask> tasks;
+  private final SagaContext sagaContext;
+  private final CompletionService<Operation> executorService;
+
+  TransactionTaskConsumer(
+      Map<String, SagaTask> tasks,
+      SagaContext sagaContext,
+      CompletionService<Operation> executorService) {
+
+    this.tasks = tasks;
+    this.sagaContext = sagaContext;
+    this.executorService = executorService;
+  }
+
+  @Segment(name = "consumeTask", category = "application", library = "kamon")
+  @Override
+  public void consume(Collection<Node<SagaRequest>> nodes) {
+    List<Future<Operation>> futures = new ArrayList<>(nodes.size());
+    for (Node<SagaRequest> node : nodes) {
+      SagaRequest request = node.value();
+      if (sagaContext.isChosenChild(request)) {
+        futures.add(futureOf(request));
+      }
+    }
+
+    for (int i = 0; i < futures.size(); i++) {
+      try {
+        executorService.take().get();
+      } catch (ExecutionException e) {
+        if (e.getCause() instanceof SagaStartFailedException) {
+          throw ((SagaStartFailedException) e.getCause());
+        }
+        throw new TransactionFailedException(e.getCause());
+      } catch (InterruptedException e) {
+        // TODO: 7/29/2017 what shall we do when system is shutting down?
+        throw new TransactionFailedException(e);
+      }
+    }
+  }
+
+  @Override
+  public boolean replay(Collection<Node<SagaRequest>> nodes) {
+    for (Iterator<Node<SagaRequest>> iterator = nodes.iterator(); iterator.hasNext(); ) {
+      SagaRequest request = iterator.next().value();
+      if (sagaContext.isTransactionCompleted(request)) {
+        log.info("Skipped completed transaction id={} operation={} while replay", request.id(), request.transaction());
+        iterator.remove();
+      }
+    }
+    return !nodes.isEmpty();
+  }
+
+  @Segment(name = "submitCallable", category = "application", library = "kamon")
+  private Future<Operation> futureOf(SagaRequest request) {
+    return executorService.submit(new OperationCallable(tasks, request, sagaContext.responseOf(request.parents())));
+  }
+
+  @EnableKamon
+  private static class OperationCallable implements Callable<Operation> {
+
+    private final SagaRequest request;
+    private final Map<String, SagaTask> tasks;
+    private final SagaResponse parentResponse;
+
+    private OperationCallable(
+        Map<String, SagaTask> tasks,
+        SagaRequest request,
+        SagaResponse parentResponse) {
+      this.request = request;
+      this.tasks = tasks;
+      this.parentResponse = parentResponse;
+    }
+
+    @Trace("runTransactionCallable")
+    @Override
+    public Operation call() throws Exception {
+      tasks.get(request.task()).commit(request, parentResponse);
+      return request.transaction();
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transport.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transport.java
new file mode 100644
index 0000000..5679e0a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transport.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public interface Transport {
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransportFailedException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransportFailedException.java
new file mode 100644
index 0000000..e01a9d5
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransportFailedException.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class TransportFailedException extends RuntimeException {
+  public TransportFailedException(String cause) {
+    super(cause);
+  }
+
+  public TransportFailedException(String cause, Throwable e) {
+    super(cause, e);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSaga.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSaga.java
new file mode 100644
index 0000000..bf60f98
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSaga.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.EventContext;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.SagaEvent;
+
+import akka.actor.ActorRef;
+
+public class ActorBasedSaga implements Saga {
+  private final ActorRef root;
+  private final ActorRef completionCallback;
+  private final CompletableFuture<SagaResponse> future;
+  private final EventStore sagaLog;
+  private final EventContext sagaContext;
+
+  ActorBasedSaga(ActorRef root, ActorRef completionCallback, CompletableFuture<SagaResponse> future, EventStore sagaLog,
+      EventContext sagaContext) {
+    this.root = root;
+    this.completionCallback = completionCallback;
+    this.future = future;
+    this.sagaLog = sagaLog;
+    this.sagaContext = sagaContext;
+  }
+
+  @Override
+  public SagaResponse run() {
+    root.tell(new TransactMessage(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE), completionCallback);
+
+    return future.join();
+  }
+
+  @Override
+  public void play() {
+    gatherEvents(sagaLog);
+  }
+
+  private void gatherEvents(Iterable<SagaEvent> events) {
+    for (SagaEvent event : events) {
+      event.gatherTo(sagaContext);
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaFactory.java
new file mode 100644
index 0000000..8ce22fa
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaFactory.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaTaskFactory;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import scala.concurrent.Await;
+import scala.concurrent.duration.Duration;
+
+public class ActorBasedSagaFactory implements SagaFactory {
+  private final ActorSystem actorSystem = ActorSystem.create("saga");
+  private final RequestActorBuilder actorBuilder;
+  private final SagaTaskFactory sagaTaskFactory;
+
+  public ActorBasedSagaFactory(int retryDelay,
+      PersistentStore persistentStore,
+      FromJsonFormat<Set<String>> childrenExtractor) {
+
+    this.sagaTaskFactory = new SagaTaskFactory(retryDelay, persistentStore);
+    this.actorBuilder = new RequestActorBuilder(actorSystem, childrenExtractor);
+  }
+
+  @Override
+  public ActorBasedSaga createSaga(String requestJson, String sagaId, EventStore sagaLog, SagaDefinition definition) {
+
+    CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+    ActorRef completionCallback = actorSystem.actorOf(CompletionCallbackActor.props(future));
+    RequestActorContext context = actorBuilder.build(
+        definition.requests(),
+        sagaTaskFactory.sagaTasks(sagaId,
+            requestJson,
+            definition.policy(),
+            sagaLog
+        ),
+        completionCallback);
+
+    completionCallback.tell(context, noSender());
+    return new ActorBasedSaga(
+        context.actorOf(NoOpSagaRequest.SAGA_START_REQUEST.id()),
+        completionCallback,
+        future,
+        sagaLog,
+        new EventContextImpl(context));
+  }
+
+  @Override
+  public boolean isTerminated() {
+    return actorSystem.whenTerminated().isCompleted();
+  }
+
+  @Override
+  public void terminate() throws Exception {
+    Await.result(actorSystem.terminate(), Duration.Inf());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActor.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActor.java
new file mode 100644
index 0000000..7cee4d3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActor.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import akka.actor.AbstractLoggingActor;
+import akka.actor.Props;
+
+class CompletionCallbackActor extends AbstractLoggingActor {
+  private final CompletableFuture<SagaResponse> future;
+
+  private CompletionCallbackActor(CompletableFuture<SagaResponse> future) {
+    this.future = future;
+  }
+
+  static Props props(CompletableFuture<SagaResponse> future) {
+    return Props.create(CompletionCallbackActor.class, () -> new CompletionCallbackActor(future));
+  }
+
+  @Override
+  public Receive createReceive() {
+    return receiveBuilder()
+        .match(RequestActorContext.class, this::ready)
+        .build();
+  }
+
+  private void ready(RequestActorContext context) {
+    getContext().become(receiveBuilder()
+        .match(CompensateMessage.class, message -> end(context, message.response()))
+        .match(TransactMessage.class, message -> end(context, message.response()))
+        .match(AbortMessage.class, message -> onAbort(context, message))
+        .match(FailMessage.class, message -> end(context, message.response()))
+        .build());
+  }
+
+  private void onAbort(RequestActorContext context, AbortMessage message) {
+    log().info("saga actor: received abort message of {}", message.response());
+    context.actorOf(NoOpSagaRequest.SAGA_END_REQUEST.id()).tell(new CompensateMessage(message.response()), self());
+  }
+
+  private void end(RequestActorContext context, SagaResponse response) {
+    log().info("saga actor: received response {}", response);
+    future.complete(response);
+    context.forAll(actor -> getContext().stop(actor));
+    getContext().stop(self());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/EventContextImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/EventContextImpl.java
new file mode 100644
index 0000000..21a2928
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/EventContextImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+
+import org.apache.servicecomb.saga.core.EventContext;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.AbortRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.Message;
+
+public class EventContextImpl implements EventContext {
+  private final RequestActorContext context;
+
+  EventContextImpl(RequestActorContext context) {
+    this.context = context;
+  }
+
+  @Override
+  public void beginTransaction(SagaRequest request) {
+
+  }
+
+  @Override
+  public void endTransaction(SagaRequest request, SagaResponse response) {
+    sendMessage(request, new TransactionRecoveryMessage(response));
+  }
+
+  @Override
+  public void abortTransaction(SagaRequest request, SagaResponse response) {
+    sendMessage(request, new AbortRecoveryMessage(response));
+  }
+
+  @Override
+  public void compensateTransaction(SagaRequest request, SagaResponse response) {
+    sendMessage(request, new CompensationRecoveryMessage());
+  }
+
+  private void sendMessage(SagaRequest request, Message message) {
+    context.actorOf(request.id()).tell(message, noSender());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActor.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActor.java
new file mode 100644
index 0000000..055174e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActor.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.apache.servicecomb.saga.core.CompositeSagaResponse;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaStartFailedException;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.apache.servicecomb.saga.core.TransactionFailedException;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.apache.servicecomb.saga.core.actors.messages.AbortRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.actors.messages.Message;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+
+import akka.actor.AbstractLoggingActor;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.japi.pf.ReceiveBuilder;
+
+public class RequestActor extends AbstractLoggingActor {
+  private final RequestActorContext context;
+  private final SagaTask task;
+  private final SagaRequest request;
+
+  private final List<SagaResponse> parentResponses;
+  private final List<ActorRef> compensatedChildren;
+
+  private final Receive transacted;
+  private final Receive aborted;
+
+  static Props props(
+      RequestActorContext context,
+      SagaTask task,
+      SagaRequest request) {
+    return Props.create(RequestActor.class, () -> new RequestActor(context, task, request));
+  }
+
+  private RequestActor(
+      RequestActorContext context,
+      SagaTask task,
+      SagaRequest request) {
+    this.context = context;
+    this.task = task;
+    this.request = request;
+    this.parentResponses = new ArrayList<>(request.parents().length);
+    this.compensatedChildren = new LinkedList<>();
+
+    this.aborted = onReceive(ignored -> {
+    }).build();
+
+    this.transacted = onReceive(task::compensate)
+        .match(CompensationRecoveryMessage.class, message -> getContext().become(aborted))
+        .build();
+  }
+
+  @Override
+  public Receive createReceive() {
+    return receiveBuilder()
+        .match(TransactMessage.class,
+            message -> onTransaction(message, () -> task.commit(request, responseOf(parentResponses))))
+        .match(TransactionRecoveryMessage.class, this::onTransactRecovery)
+        .match(AbortRecoveryMessage.class, this::onAbortRecovery)
+        .match(AbortMessage.class, this::onAbort)
+        .build();
+  }
+
+  private void onAbort(AbortMessage message) {
+    log().debug("{}: received abort message of {}", request.id(), message.response());
+    sendToChildrenButSender(message);
+    sendToParentsButSender(message);
+
+    getContext().become(aborted);
+  }
+
+  private void sendToParentsButSender(AbortMessage message) {
+    context.parentsOf(request)
+        .stream()
+        .filter(this::isNotSender)
+        .forEach(actor -> actor.tell(message, self()));
+  }
+
+  private void sendToChildrenButSender(AbortMessage message) {
+    context.childrenOf(request)
+        .stream()
+        .filter(this::isNotSender)
+        .forEach(actor -> actor.tell(message, self()));
+  }
+
+  private boolean isNotSender(ActorRef actor) {
+    return !actor.equals(sender());
+  }
+
+  private void onTransactRecovery(TransactionRecoveryMessage message) {
+    getContext().become(receiveBuilder()
+        .match(TransactMessage.class, m -> onTransaction(m, message::response))
+        .match(CompensationRecoveryMessage.class, m -> getContext().become(aborted))
+        .build()
+    );
+  }
+
+  private void onAbortRecovery(AbortRecoveryMessage message) {
+    getContext().become(
+        receiveBuilder()
+            .match(TransactMessage.class, m -> onAbort(new AbortMessage(message.response())))
+            .build());
+  }
+
+  private void onTransaction(TransactMessage message, Supplier<SagaResponse> responseSupplier) {
+    log().debug("{}: received transaction message of {}", request.id(), message.request());
+    if (context.parentsOf(request).contains(sender())) {
+      parentResponses.add(message.response());
+    }
+
+    if (parentResponses.size() == context.parentsOf(request).size()) {
+      transact(responseSupplier);
+    }
+  }
+
+  private void transact(Supplier<SagaResponse> responseSupplier) {
+    try {
+      if (isChosenChild(parentResponses)) {
+        SagaResponse sagaResponse = responseSupplier.get();
+        sendToChildren(new TransactMessage(request, sagaResponse));
+        getContext().become(transacted);
+      } else {
+        sendToChildren(new TransactMessage(request, SagaResponse.NONE_RESPONSE));
+        getContext().become(aborted);
+      }
+    } catch (SagaStartFailedException e) {
+      sendToParents(new FailMessage(e));
+    } catch (Exception e) {
+      log().error("Failed to run operation {} with error {}", request.transaction(), e);
+
+      Message abortMessage = new AbortMessage(new TransactionFailedException(e));
+      sendToParents(abortMessage);
+      sendToChildren(abortMessage);
+      getContext().become(aborted);
+    }
+  }
+
+  private void sendToParents(Message message) {
+    context.parentsOf(request).forEach(actor -> actor.tell(message, self()));
+  }
+
+  private void sendToChildren(Message message) {
+    context.childrenOf(request).forEach(actor -> actor.tell(message, self()));
+  }
+
+  private boolean isChosenChild(List<SagaResponse> parentResponses) {
+    return request.parents().length == 0 || parentResponses.isEmpty() || parentResponses.stream()
+            .map(context::chosenChildren)
+            .anyMatch(chosenChildren -> chosenChildren.isEmpty() || chosenChildren.contains(request.id()));
+  }
+
+  private SagaResponse responseOf(List<SagaResponse> responseContexts) {
+    if (responseContexts.isEmpty()) {
+      return SagaResponse.EMPTY_RESPONSE;
+    }
+
+    if (responseContexts.size() == 1) {
+      return responseContexts.get(0);
+    }
+    return new CompositeSagaResponse(responseContexts);
+  }
+
+  private ReceiveBuilder onReceive(Consumer<SagaRequest> requestConsumer) {
+    return receiveBuilder()
+        .match(CompensateMessage.class, message -> onCompensate(message, requestConsumer));
+  }
+
+  private void onCompensate(CompensateMessage message, Consumer<SagaRequest> requestConsumer) {
+    log().debug("{}: received compensation message from {}", request.id(), sender());
+    compensatedChildren.add(sender());
+
+    if (compensatedChildren.size() == context.childrenOf(request).size()) {
+      requestConsumer.accept(request);
+      sendToParents(message);
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilder.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilder.java
new file mode 100644
index 0000000..5004b4e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.actor.Props;
+
+public class RequestActorBuilder {
+  private final ActorSystem actorSystem;
+  private final FromJsonFormat<Set<String>> childrenExtractor;
+
+  RequestActorBuilder(
+      ActorSystem actorSystem,
+      FromJsonFormat<Set<String>> childrenExtractor) {
+
+    this.actorSystem = actorSystem;
+    this.childrenExtractor = childrenExtractor;
+  }
+
+  public RequestActorContext build(SagaRequest[] requests, Map<String, SagaTask> tasks, ActorRef sagaActor) {
+    RequestActorContext context = new RequestActorContext(childrenExtractor);
+
+    ActorRef rootActor = rootActor(context, tasks);
+    ActorRef leafActor = leafActor(context, tasks);
+
+    createRequestActors(requests, tasks, context);
+
+    linkActorsById(rootActor, requests, context);
+    addLeafToChildless(leafActor, requests, context);
+
+    context.addParent(NoOpSagaRequest.SAGA_START_REQUEST.id(), sagaActor);
+    context.addChild(NoOpSagaRequest.SAGA_END_REQUEST.id(), sagaActor);
+    return context;
+  }
+
+  private void linkActorsById(ActorRef rootActor, SagaRequest[] requests, RequestActorContext context) {
+    for (SagaRequest request : requests) {
+      if (isOrphan(request)) {
+        context.addParent(request.id(), rootActor);
+        context.addChild(NoOpSagaRequest.SAGA_START_REQUEST.id(), context.actorOf(request.id()));
+      } else {
+        for (String parent : request.parents()) {
+          context.addParent(request.id(), context.actorOf(parent));
+          context.addChild(parent, context.actorOf(request.id()));
+        }
+      }
+    }
+  }
+
+  private boolean isOrphan(SagaRequest request) {
+    return request.parents().length == 0;
+  }
+
+  private void createRequestActors(SagaRequest[] requests, Map<String, SagaTask> tasks, RequestActorContext context) {
+    for (SagaRequest request : requests) {
+      Props props = RequestActor.props(context, tasks.get(request.task()), request);
+      context.addActor(request.id(), actorSystem.actorOf(props));
+    }
+  }
+
+  private void addLeafToChildless(ActorRef leafActor, SagaRequest[] requests, RequestActorContext context) {
+    for (SagaRequest request : requests) {
+      if (context.childrenOf(request).isEmpty()) {
+        context.addParent(NoOpSagaRequest.SAGA_END_REQUEST.id(), context.actorOf(request.id()));
+        context.addChild(request.id(), leafActor);
+      }
+    }
+  }
+
+  private ActorRef rootActor(RequestActorContext context, Map<String, SagaTask> tasks) {
+    Props root = RequestActor.props(context, tasks.get(
+        NoOpSagaRequest.SAGA_START_REQUEST.task()), NoOpSagaRequest.SAGA_START_REQUEST);
+    ActorRef actor = actorSystem.actorOf(root);
+    context.addActor(NoOpSagaRequest.SAGA_START_REQUEST.id(), actor);
+    return actor;
+  }
+
+  private ActorRef leafActor(RequestActorContext context, Map<String, SagaTask> tasks) {
+    Props leaf = RequestActor.props(context, tasks.get(
+        NoOpSagaRequest.SAGA_END_REQUEST.task()), NoOpSagaRequest.SAGA_END_REQUEST);
+    ActorRef actor = actorSystem.actorOf(leaf);
+    context.addActor(NoOpSagaRequest.SAGA_END_REQUEST.id(), actor);
+    return actor;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorContext.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorContext.java
new file mode 100644
index 0000000..2e3149d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorContext.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static java.util.Collections.emptyList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+
+class RequestActorContext {
+  private final Map<String, ActorRef> actors;
+  private final Map<String, List<ActorRef>> parents;
+  private final Map<String, List<ActorRef>> children;
+  private final FromJsonFormat<Set<String>> childrenExtractor;
+
+  RequestActorContext(
+      FromJsonFormat<Set<String>> childrenExtractor) {
+    this.actors = new HashMap<>();
+    this.children = new HashMap<>();
+    this.parents = new HashMap<>();
+    this.childrenExtractor = childrenExtractor;
+  }
+
+  void addActor(String id, ActorRef actorRef) {
+    actors.put(id, actorRef);
+  }
+
+  void addChild(String requestId, ActorRef ref) {
+    children.computeIfAbsent(requestId, k -> new ArrayList<>()).add(ref);
+  }
+
+  void addParent(String requestId, ActorRef ref) {
+    parents.computeIfAbsent(requestId, k -> new ArrayList<>()).add(ref);
+  }
+
+  ActorRef actorOf(String id) {
+    return actors.get(id);
+  }
+
+  Collection<ActorRef> parentsOf(SagaRequest request) {
+    return parents.getOrDefault(request.id(), emptyList());
+  }
+
+  Collection<ActorRef> childrenOf(SagaRequest request) {
+    return children.getOrDefault(request.id(), emptyList());
+  }
+
+  void forAll(Consumer<ActorRef> consumer) {
+    actors.values()
+        .stream()
+        .forEach(consumer);
+  }
+
+  Set<String> chosenChildren(SagaResponse response) {
+    return childrenExtractor.fromJson(response.body());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortMessage.java
new file mode 100644
index 0000000..f23a079
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortMessage.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class AbortMessage implements Message {
+  private final SagaResponse response;
+
+  public AbortMessage(Throwable e) {
+    response = new FailedSagaResponse(e);
+  }
+
+  public AbortMessage(SagaResponse response) {
+    this.response = response;
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortRecoveryMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortRecoveryMessage.java
new file mode 100644
index 0000000..951157d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortRecoveryMessage.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class AbortRecoveryMessage implements Message {
+
+  private final SagaResponse response;
+
+  public AbortRecoveryMessage(SagaResponse response) {
+    this.response = response;
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensateMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensateMessage.java
new file mode 100644
index 0000000..5fd0701
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensateMessage.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class CompensateMessage implements Message {
+  private final SagaResponse response;
+
+  public CompensateMessage(SagaResponse response) {
+    this.response = response;
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensationRecoveryMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensationRecoveryMessage.java
new file mode 100644
index 0000000..421a092
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensationRecoveryMessage.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+public class CompensationRecoveryMessage implements Message {
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/FailMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/FailMessage.java
new file mode 100644
index 0000000..7a80ecb
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/FailMessage.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class FailMessage implements Message {
+  private final SagaResponse response;
+
+  public FailMessage(Throwable e) {
+    response = new FailedSagaResponse(e);
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/Message.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/Message.java
new file mode 100644
index 0000000..3ed8187
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/Message.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+public interface Message {
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactMessage.java
new file mode 100644
index 0000000..e4c57a2
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactMessage.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class TransactMessage implements Message {
+  private final SagaRequest request;
+  private final SagaResponse response;
+
+  public TransactMessage(SagaRequest request, SagaResponse response) {
+    this.request = request;
+    this.response = response;
+  }
+
+  public SagaRequest request() {
+    return request;
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactionRecoveryMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactionRecoveryMessage.java
new file mode 100644
index 0000000..26ba05b
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactionRecoveryMessage.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class TransactionRecoveryMessage implements Message {
+  private final SagaResponse response;
+
+  public TransactionRecoveryMessage(SagaResponse response) {
+    this.response = response;
+  }
+
+  public SagaResponse response() {
+    return response;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaExecutionComponent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaExecutionComponent.java
new file mode 100644
index 0000000..7a600d7
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaExecutionComponent.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.application;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.ToJsonFormat;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaEvent;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class SagaExecutionComponent {
+
+  private final PersistentStore persistentStore;
+  private final FromJsonFormat<SagaDefinition> fromJsonFormat;
+  private final ToJsonFormat toJsonFormat;
+  private final SagaFactory sagaFactory;
+
+  public SagaExecutionComponent(
+      PersistentStore persistentStore,
+      FromJsonFormat<SagaDefinition> fromJsonFormat,
+      ToJsonFormat toJsonFormat,
+      SagaFactory sagaFactory) {
+    this.persistentStore = persistentStore;
+    this.fromJsonFormat = fromJsonFormat;
+    this.toJsonFormat = toJsonFormat;
+    this.sagaFactory = sagaFactory;
+  }
+
+  @Segment(name = "runSagaExecutionComponent", category = "application", library = "kamon")
+  public SagaResponse run(String requestJson) {
+    String sagaId = UUID.randomUUID().toString();
+    EventStore sagaLog = new EmbeddedEventStore();
+    SagaDefinition definition = fromJsonFormat.fromJson(requestJson);
+    Saga saga = sagaFactory.createSaga(requestJson, sagaId, sagaLog, definition);
+    return saga.run();
+  }
+
+  public void reanimate() {
+    Map<String, List<EventEnvelope>> pendingSagaEvents = persistentStore.findPendingSagaEvents();
+
+    for (Entry<String, List<EventEnvelope>> entry : pendingSagaEvents.entrySet()) {
+      EventStore eventStore = new EmbeddedEventStore();
+      eventStore.populate(entry.getValue());
+      SagaEvent event = entry.getValue().iterator().next().event;
+
+      String requestJson = event.json(toJsonFormat);
+      SagaDefinition definition = fromJsonFormat.fromJson(requestJson);
+
+      Saga saga = sagaFactory.createSaga(requestJson, event.sagaId, eventStore, definition);
+
+      saga.play();
+      saga.run();
+    }
+  }
+
+  public void terminate() throws Exception {
+    sagaFactory.terminate();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaFactory.java
new file mode 100644
index 0000000..b60b5e7
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.application;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+
+public interface SagaFactory {
+  Saga createSaga(String requestJson, String sagaId, EventStore sagaLog, SagaDefinition definition);
+
+  boolean isTerminated();
+
+  void terminate() throws Exception;
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/FromJsonFormat.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/FromJsonFormat.java
new file mode 100644
index 0000000..ccfb408
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/FromJsonFormat.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.application.interpreter;
+
+public interface FromJsonFormat<T> {
+
+  T fromJson(String requestJson);
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/RestRequestChecker.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/RestRequestChecker.java
new file mode 100644
index 0000000..7a65e48
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/RestRequestChecker.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.application.interpreter;
+
+import static java.util.Arrays.asList;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class RestRequestChecker {
+
+  private static final Set<String> validMethods = new HashSet<>(asList(
+      "GET",
+      "POST",
+      "PUT",
+      "DELETE"
+  ));
+
+  private RestRequestChecker() {
+  }
+
+  public static void checkParameters(String method, Map<String, Map<String, String>> params) {
+    if (method == null || !validMethods.contains(method.toUpperCase())) {
+      throw new IllegalArgumentException("Unsupported method " + method);
+    }
+
+    if (isDeleteOrGet(method) && hasBody(params)) {
+      throw new IllegalArgumentException("GET & DELETE request cannot enclose a body");
+    }
+  }
+
+  private static boolean isDeleteOrGet(String method) {
+    return "GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method);
+  }
+
+  private static boolean hasBody(Map<String, Map<String, String>> params) {
+    return params != null && (params.containsKey("form") || params.containsKey("json"));
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/ByLevelTraveller.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/ByLevelTraveller.java
new file mode 100644
index 0000000..22bc874
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/ByLevelTraveller.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class ByLevelTraveller<T> implements Traveller<T> {
+
+  private final Collection<Node<T>> nodes;
+  private final Collection<Node<T>> nodesBuffer;
+
+  private final Queue<Node<T>> nodesWithoutParent = new LinkedList<>();
+  private final Map<Long, Set<Node<T>>> nodeParents = new HashMap<>();
+  private final TraversalDirection<T> traversalDirection;
+
+
+  public ByLevelTraveller(SingleLeafDirectedAcyclicGraph<T> dag, TraversalDirection<T> traversalDirection) {
+    this.nodes = new LinkedHashSet<>();
+    this.nodesBuffer = new LinkedList<>();
+    this.traversalDirection = traversalDirection;
+
+    nodesWithoutParent.offer(traversalDirection.root(dag));
+  }
+
+  @Segment(name = "travelNext", category = "application", library = "kamon")
+  @Override
+  public void next() {
+    nodes.addAll(nodesBuffer);
+    nodesBuffer.clear();
+    boolean buffered = false;
+
+    while (!nodesWithoutParent.isEmpty() && !buffered) {
+      Node<T> node = nodesWithoutParent.poll();
+      nodes.add(node);
+
+      for (Node<T> child : traversalDirection.children(node)) {
+        nodeParents.computeIfAbsent(child.id(), id -> new HashSet<>(traversalDirection.parents(child)));
+        nodeParents.get(child.id()).remove(node);
+
+        if (nodeParents.get(child.id()).isEmpty()) {
+          nodesWithoutParent.offer(child);
+          nodesBuffer.add(child);
+          buffered = true;
+        }
+      }
+    }
+  }
+
+  @Override
+  public boolean hasNext() {
+    return !nodesWithoutParent.isEmpty();
+  }
+
+  @Override
+  public Collection<Node<T>> nodes() {
+    return nodes;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromLeafTraversalDirection.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromLeafTraversalDirection.java
new file mode 100644
index 0000000..d97c40f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromLeafTraversalDirection.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Set;
+
+public class FromLeafTraversalDirection<T> implements TraversalDirection<T> {
+
+  @Override
+  public Node<T> root(SingleLeafDirectedAcyclicGraph<T> dag) {
+    return dag.leaf();
+  }
+
+  @Override
+  public Set<Node<T>> parents(Node<T> node) {
+    return node.children();
+  }
+
+  @Override
+  public Set<Node<T>> children(Node<T> node) {
+    return node.parents();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromRootTraversalDirection.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromRootTraversalDirection.java
new file mode 100644
index 0000000..8c664ac
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromRootTraversalDirection.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Set;
+
+public class FromRootTraversalDirection<T> implements TraversalDirection<T> {
+
+  @Override
+  public Node<T> root(SingleLeafDirectedAcyclicGraph<T> dag) {
+    return dag.root();
+  }
+
+  @Override
+  public Set<Node<T>> parents(Node<T> node) {
+    return node.parents();
+  }
+
+  @Override
+  public Set<Node<T>> children(Node<T> node) {
+    return node.children();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaFactory.java
new file mode 100644
index 0000000..2048383
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaFactory.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.GraphBasedSaga;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaContext;
+import org.apache.servicecomb.saga.core.SagaContextImpl;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.apache.servicecomb.saga.infrastructure.ContextAwareEventStore;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaTaskFactory;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+
+public class GraphBasedSagaFactory implements SagaFactory {
+  private final AtomicBoolean isRunning = new AtomicBoolean(true);
+  private final FromJsonFormat<Set<String>> childrenExtractor;
+  private final Executor executorService;
+  private final GraphBuilder graphBuilder;
+  private final SagaTaskFactory sagaTaskFactory;
+
+  public GraphBasedSagaFactory(int retryDelay,
+      PersistentStore persistentStore,
+      FromJsonFormat<Set<String>> childrenExtractor,
+      ExecutorService executorService) {
+
+    this.childrenExtractor = childrenExtractor;
+    this.executorService = executorService;
+    this.sagaTaskFactory = new SagaTaskFactory(retryDelay, persistentStore);
+    this.graphBuilder = new GraphBuilder(new GraphCycleDetectorImpl<>());
+  }
+
+  @Override
+  public Saga createSaga(String requestJson, String sagaId, EventStore sagaLog, SagaDefinition definition) {
+    SagaContext sagaContext = new SagaContextImpl(childrenExtractor);
+
+    return new GraphBasedSaga(
+        sagaLog,
+        executorService,
+        sagaTaskFactory.sagaTasks(sagaId,
+            requestJson,
+            definition.policy(),
+            new ContextAwareEventStore(sagaLog, sagaContext)
+        ),
+        sagaContext,
+        graphBuilder.build(definition.requests()));
+  }
+
+  @Override
+  public boolean isTerminated() {
+    return !isRunning.get();
+  }
+
+  @Override
+  public void terminate() throws Exception {
+    isRunning.compareAndSet(true, false);
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBuilder.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBuilder.java
new file mode 100644
index 0000000..578cbb9
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBuilder.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaException;
+import org.apache.servicecomb.saga.core.SagaRequest;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class GraphBuilder {
+
+  private final GraphCycleDetector<SagaRequest> detector;
+
+  public GraphBuilder(GraphCycleDetector<SagaRequest> detector) {
+    this.detector = detector;
+  }
+
+  @Segment(name = "buildGraph", category = "application", library = "kamon")
+  public SingleLeafDirectedAcyclicGraph<SagaRequest> build(SagaRequest[] sagaRequests) {
+    Map<String, Node<SagaRequest>> requestNodes = requestsToNodes(sagaRequests);
+
+    SingleLeafDirectedAcyclicGraph<SagaRequest> graph = linkNodesToGraph(sagaRequests, requestNodes);
+    detectCycle(graph);
+    return graph;
+  }
+
+  private SingleLeafDirectedAcyclicGraph<SagaRequest> linkNodesToGraph(
+      SagaRequest[] sagaRequests,
+      Map<String, Node<SagaRequest>> requestNodes) {
+
+    Node<SagaRequest> root = rootNode(0);
+    Node<SagaRequest> leaf = leafNode(sagaRequests.length + 1);
+
+    for (SagaRequest sagaRequest : sagaRequests) {
+      if (isOrphan(sagaRequest)) {
+        root.addChild(requestNodes.get(sagaRequest.id()));
+      } else {
+        for (String parent : sagaRequest.parents()) {
+          requestNodes.get(parent).addChild(requestNodes.get(sagaRequest.id()));
+        }
+      }
+    }
+
+    requestNodes.values().stream()
+        .filter((node) -> node.children().isEmpty())
+        .forEach(node -> node.addChild(leaf));
+
+    return new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+  }
+
+  private Node<SagaRequest> rootNode(int id) {
+    return new Node<>(
+        id,
+        NoOpSagaRequest.SAGA_START_REQUEST);
+  }
+
+  private Node<SagaRequest> leafNode(int id) {
+    return new Node<>(
+        id,
+        NoOpSagaRequest.SAGA_END_REQUEST);
+  }
+
+  private boolean isOrphan(SagaRequest sagaRequest) {
+    return sagaRequest.parents().length == 0;
+  }
+
+  private Map<String, Node<SagaRequest>> requestsToNodes(SagaRequest[] sagaRequests) {
+    long index = 1;
+    Map<String, Node<SagaRequest>> requestMap = new HashMap<>();
+    for (SagaRequest sagaRequest : sagaRequests) {
+      if (requestMap.containsKey(sagaRequest.id())) {
+        // TODO: 8/20/2017 add random id if user didn't provide one
+        throw new SagaException("Failed to interpret requests with duplicate request id: " + sagaRequest.id());
+      }
+      requestMap.put(sagaRequest.id(), new Node<>(index++, sagaRequest));
+    }
+    return requestMap;
+  }
+
+  private void detectCycle(SingleLeafDirectedAcyclicGraph<SagaRequest> graph) {
+    Set<Node<SagaRequest>> jointNodes = detector.cycleJoints(graph);
+
+    if (!jointNodes.isEmpty()) {
+      throw new SagaException("Cycle detected in the request graph at nodes " + jointNodes);
+    }
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetector.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetector.java
new file mode 100644
index 0000000..9b12bc8
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetector.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Set;
+
+public interface GraphCycleDetector<T> {
+
+  Set<Node<T>> cycleJoints(SingleLeafDirectedAcyclicGraph<T> graph);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorImpl.java
new file mode 100644
index 0000000..6011a98
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorImpl.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Cycle detection is based on topological sort with Kahn's algorithm.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Topological_sorting">Topological Sorting</a>
+ */
+public class GraphCycleDetectorImpl<T> implements GraphCycleDetector<T> {
+
+  @Override
+  public Set<Node<T>> cycleJoints(SingleLeafDirectedAcyclicGraph<T> graph) {
+    Queue<Node<T>> orphanNodes = new LinkedList<>();
+    Map<Node<T>, Set<Node<T>>> nodeParents = new HashMap<>();
+
+    orphanNodes.add(graph.root());
+
+    traverse(orphanNodes, nodeParents);
+
+    return unvisitedNodes(nodeParents);
+  }
+
+  private void traverse(Queue<Node<T>> orphanNodes, Map<Node<T>, Set<Node<T>>> nodeParents) {
+    while (!orphanNodes.isEmpty()) {
+      Node<T> node = orphanNodes.poll();
+
+      node.children().forEach(child -> {
+        nodeParents.computeIfAbsent(child, n -> new HashSet<>(child.parents()))
+            .remove(node);
+
+        if (nodeParents.get(child).isEmpty()) {
+          orphanNodes.add(child);
+        }
+      });
+    }
+  }
+
+  private Set<Node<T>> unvisitedNodes(Map<Node<T>, Set<Node<T>>> nodeParents) {
+    return nodeParents.entrySet()
+        .parallelStream()
+        .filter(parents -> !parents.getValue().isEmpty())
+        .map(Entry::getKey)
+        .collect(Collectors.toSet());
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Node.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Node.java
new file mode 100644
index 0000000..0b3be0a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Node.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class Node<T> {
+  private final long id;
+  private final T value;
+  private final Set<Node<T>> children = new HashSet<>();
+  private final Set<Node<T>> parents = new HashSet<>();
+
+  public Node(long id, T value) {
+    this.id = id;
+    this.value = value;
+  }
+
+  long id() {
+    return id;
+  }
+
+  public T value() {
+    return value;
+  }
+
+  Set<Node<T>> parents() {
+    return parents;
+  }
+
+  public Set<Node<T>> children() {
+    return children;
+  }
+
+  public void addChild(Node<T> node) {
+    children.add(node);
+    node.parents.add(this);
+  }
+
+  public void addChildren(Collection<Node<T>> nodes) {
+    children.addAll(nodes);
+    nodes.forEach(node -> node.parents.add(this));
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Node<?> node = (Node<?>) o;
+    return id == node.id;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(id);
+  }
+
+  @Override
+  public String toString() {
+    return "Node{" +
+        "id=" + id +
+        ", value=" + value +
+        '}';
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/SingleLeafDirectedAcyclicGraph.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/SingleLeafDirectedAcyclicGraph.java
new file mode 100644
index 0000000..cf0d19b
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/SingleLeafDirectedAcyclicGraph.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+public class SingleLeafDirectedAcyclicGraph<T> {
+
+  private final Node<T> root;
+  private final Node<T> leaf;
+
+  public SingleLeafDirectedAcyclicGraph(Node<T> root, Node<T> leaf) {
+
+    this.root = root;
+    this.leaf = leaf;
+  }
+
+  public Node<T> root() {
+    return root;
+  }
+
+  Node<T> leaf() {
+    return leaf;
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Traveller.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Traveller.java
new file mode 100644
index 0000000..e44848b
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Traveller.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Collection;
+
+public interface Traveller<T> {
+
+  void next();
+
+  boolean hasNext();
+
+  Collection<Node<T>> nodes();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/TraversalDirection.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/TraversalDirection.java
new file mode 100644
index 0000000..22f7343
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/TraversalDirection.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.Set;
+
+public interface TraversalDirection<T> {
+
+  Node<T> root(SingleLeafDirectedAcyclicGraph<T> dag);
+
+  Set<Node<T>> parents(Node<T> node);
+
+  Set<Node<T>> children(Node<T> node);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStore.java
new file mode 100644
index 0000000..40fa71d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStore.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.infrastructure;
+
+import java.util.Iterator;
+
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.SagaContext;
+import org.apache.servicecomb.saga.core.SagaEvent;
+
+public class ContextAwareEventStore implements EventStore {
+  private final EventStore eventStore;
+  private final SagaContext sagaContext;
+
+  public ContextAwareEventStore(EventStore eventStore, SagaContext sagaContext) {
+    this.eventStore = eventStore;
+    this.sagaContext = sagaContext;
+  }
+
+  @Override
+  public void offer(SagaEvent sagaEvent) {
+    sagaEvent.gatherTo(sagaContext);
+    eventStore.offer(sagaEvent);
+  }
+
+  @Override
+  public void populate(Iterable<EventEnvelope> events) {
+    eventStore.populate(events);
+  }
+
+  @Override
+  public long size() {
+    return eventStore.size();
+  }
+
+  @Override
+  public Iterator<SagaEvent> iterator() {
+    return eventStore.iterator();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/EmbeddedEventStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/EmbeddedEventStore.java
new file mode 100644
index 0000000..d342264
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/EmbeddedEventStore.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.infrastructure;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Iterator;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.SagaEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EmbeddedEventStore implements EventStore {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private final Queue<SagaEvent> events = new LinkedBlockingQueue<>();
+
+  @Override
+  public void offer(SagaEvent sagaEvent) {
+    events.offer(sagaEvent);
+    log.info("Added event {}", sagaEvent);
+  }
+
+  @Override
+  public void populate(Iterable<EventEnvelope> events) {
+    for (EventEnvelope event : events) {
+      this.events.offer(event.event);
+      log.info("Populated event {}", event);
+    }
+  }
+
+  @Override
+  public long size() {
+    return events.size();
+  }
+
+  @Override
+  public Iterator<SagaEvent> iterator() {
+    return events.iterator();
+  }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/transports/RestTransport.java b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/RestTransport.java
new file mode 100644
index 0000000..3d5bda3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/RestTransport.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.transports;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.Transport;
+import java.util.Map;
+
+public interface RestTransport extends Transport {
+
+  SagaResponse with(String address, String path, String method, Map<String, Map<String, String>> params);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/transports/TransportFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/TransportFactory.java
new file mode 100644
index 0000000..b34b5bd
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/TransportFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.transports;
+
+public interface TransportFactory {
+
+  RestTransport restTransport();
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/BackwardRecoveryTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/BackwardRecoveryTest.java
new file mode 100644
index 0000000..4f03992
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/BackwardRecoveryTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class BackwardRecoveryTest {
+
+  private final String serviceName = "aaa";
+  private final Transaction transaction = mock(Transaction.class);
+  private final SagaTask sagaTask = mock(SagaTask.class);
+  private final SagaRequest sagaRequest = mock(SagaRequest.class);
+  private final SagaResponse parentResponse = mock(SagaResponse.class);
+  private final BackwardRecovery backwardRecovery = new BackwardRecovery();
+  private final RuntimeException exception = new RuntimeException("oops");
+
+  @Before
+  public void setUp() throws Exception {
+    when(sagaRequest.serviceName()).thenReturn(serviceName);
+    when(sagaRequest.transaction()).thenReturn(transaction);
+  }
+
+  @Test
+  public void blowsUpWhenTaskIsNotCommitted() {
+    doThrow(exception).when(transaction).send(serviceName, parentResponse);
+
+    try {
+      backwardRecovery.apply(sagaTask, sagaRequest, parentResponse);
+      expectFailing(RuntimeException.class);
+    } catch (RuntimeException ignored) {
+    }
+
+    verify(sagaTask).abort(sagaRequest, exception);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompensationImpl.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompensationImpl.java
new file mode 100644
index 0000000..c974bc4
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompensationImpl.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.Map;
+
+public class CompensationImpl extends RestOperation implements Compensation {
+
+  public CompensationImpl(String path, String method, Map<String, Map<String, String>> params) {
+    super(path, method, params);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaLogTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaLogTest.java
new file mode 100644
index 0000000..c7dea51
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaLogTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+
+public class CompositeSagaLogTest {
+
+  private final SagaRequest request = mock(SagaRequest.class);
+  private final DummyEvent sagaEvent = new DummyEvent(request);
+  private final SagaLog embedded = mock(SagaLog.class);
+  private final SagaLog persistent = mock(SagaLog.class);
+
+  private final SagaLog compositeSagaLog = new CompositeSagaLog(embedded, persistent);
+
+  @Test
+  public void addsLogsToEmbeddedOnlyAfterPersisted() {
+    doThrow(RuntimeException.class).when(persistent).offer(sagaEvent);
+
+    try {
+      compositeSagaLog.offer(sagaEvent);
+      expectFailing(RuntimeException.class);
+    } catch (RuntimeException ignored) {
+    }
+
+    verify(embedded, never()).offer(sagaEvent);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaResponseTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaResponseTest.java
new file mode 100644
index 0000000..be4b9f4
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaResponseTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class CompositeSagaResponseTest {
+
+  private final SagaResponse response1 = Mockito.mock(SagaResponse.class);
+  private final SagaResponse response2 = Mockito.mock(SagaResponse.class);
+
+  private final SagaResponse compositeSagaResponse = new CompositeSagaResponse(asList(response1, response2));
+
+  @Test
+  public void succeededOnlyWhenAllAreSuccessful() throws Exception {
+    when(response1.succeeded()).thenReturn(true);
+    when(response2.succeeded()).thenReturn(true);
+
+    assertThat(compositeSagaResponse.succeeded(), is(true));
+  }
+
+  @Test
+  public void failedWhenAnyIsNotSuccessful() throws Exception {
+    when(response1.succeeded()).thenReturn(true);
+    when(response2.succeeded()).thenReturn(false);
+
+    assertThat(compositeSagaResponse.succeeded(), is(false));
+  }
+
+  @Test
+  public void bodyCombinesAllResponseBodies() throws Exception {
+    when(response1.body()).thenReturn("{\n"
+        + "  \"status\": 500,\n"
+        + "  \"body\" : \"oops\"\n"
+        + "}\n");
+
+    when(response2.body()).thenReturn("{\n"
+        + "  \"status\": 200,\n"
+        + "  \"body\" : \"blah\"\n"
+        + "}\n");
+
+    assertThat(compositeSagaResponse.body(), is("[{\n"
+        + "  \"status\": 500,\n"
+        + "  \"body\" : \"oops\"\n"
+        + "}\n"
+        + ", {\n"
+        + "  \"status\": 200,\n"
+        + "  \"body\" : \"blah\"\n"
+        + "}\n"
+        + "]"));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/DummyEvent.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/DummyEvent.java
new file mode 100644
index 0000000..c775afe
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/DummyEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+public class DummyEvent extends SagaEvent {
+
+  DummyEvent(SagaRequest payload) {
+    super("0", payload);
+  }
+
+  @Override
+  public void gatherTo(EventContext sagaContext) {
+
+  }
+
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/FallbackPolicyTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/FallbackPolicyTest.java
new file mode 100644
index 0000000..a2d1f32
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/FallbackPolicyTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class FallbackPolicyTest {
+
+  private final int numberOfRetries = 3;
+  private final String address = uniquify("address");
+
+  private final SagaResponse success = Mockito.mock(SagaResponse.class);
+  private final SagaResponse failure = Mockito.mock(SagaResponse.class);
+  private final Fallback fallback = Mockito.mock(Fallback.class);
+  private final Compensation compensation = Mockito.mock(Compensation.class);
+  private final FallbackPolicy fallbackPolicy = new FallbackPolicy(100);
+
+  @SuppressWarnings("ThrowableInstanceNeverThrown")
+  private final RuntimeException exception = new RuntimeException("oops");
+
+  @Before
+  public void setUp() throws Exception {
+    when(compensation.retries()).thenReturn(numberOfRetries);
+    when(fallback.send(address)).thenReturn(failure);
+  }
+
+  @Test
+  public void retriesTransportForSpecifiedTimes() {
+    when(compensation.send(address))
+        .thenThrow(exception)
+        .thenThrow(exception)
+        .thenReturn(success);
+
+    SagaResponse response = fallbackPolicy.apply(address, compensation, fallback);
+
+    assertThat(response, is(success));
+    verify(compensation, times(3)).send(address);
+  }
+
+  @Test
+  public void fallbackIfTransportFailedWithRetry() {
+    when(compensation.send(address)).thenThrow(exception);
+
+    SagaResponse response = fallbackPolicy.apply(address, compensation, fallback);
+    assertThat(response, is(failure));
+
+    verify(compensation, times(numberOfRetries + 1)).send(address);
+    verify(fallback).send(address);
+  }
+
+  @Test
+  public void retryUntilSuccessIfNumberOfRetriesIsNegative() throws InterruptedException {
+    reset(compensation);
+    when(compensation.retries()).thenReturn(-1);
+    when(compensation.send(address))
+        .thenThrow(exception, exception, exception, exception, exception)
+        .thenReturn(success);
+
+    SagaResponse response = fallbackPolicy.apply(address, compensation, fallback);
+
+    assertThat(response, is(success));
+    verify(fallback, never()).send(anyString());
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/ForwardRecoveryTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/ForwardRecoveryTest.java
new file mode 100644
index 0000000..a6e1dca
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/ForwardRecoveryTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class ForwardRecoveryTest {
+
+  private final SagaTask sagaTask = mock(SagaTask.class);
+
+  private final Transaction transaction = mock(Transaction.class);
+  private final SagaRequest sagaRequest = mock(SagaRequest.class);
+  private final SagaResponse parentResponse = mock(SagaResponse.class);
+
+  private final ForwardRecovery forwardRecovery = new ForwardRecovery();
+
+  private final String serviceName = "aaa";
+
+  @Before
+  public void setUp() {
+    when(sagaRequest.serviceName()).thenReturn(serviceName);
+    when(sagaRequest.transaction()).thenReturn(transaction);
+    when(sagaRequest.failRetryDelayMilliseconds()).thenReturn(300);
+  }
+
+  @Test
+  public void blowsUpWhenTaskIsNotCommittedWithFailRetryDelaySeconds() throws Exception {
+    doThrow(Exception.class).when(transaction).send(serviceName, parentResponse);
+
+    Thread t = new Thread(() -> forwardRecovery.apply(sagaTask, sagaRequest, parentResponse));
+    t.start();
+    Thread.sleep(400);
+    t.interrupt();
+
+    verify(transaction, times(2)).send(serviceName, parentResponse);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/LongIdGeneratorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/LongIdGeneratorTest.java
new file mode 100644
index 0000000..775866b
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/LongIdGeneratorTest.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+public class LongIdGeneratorTest {
+
+
+  private final LongIdGenerator generator = new LongIdGenerator();
+
+  @Test
+  public void generatesLongInSequence() {
+    for (long index = 0; index < 10; index++) {
+      assertThat(generator.nextId(), is(index + 1));
+    }
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/RestOperationTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RestOperationTest.java
new file mode 100644
index 0000000..7f1990d
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RestOperationTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+public class RestOperationTest {
+
+  @Test
+  public void blowsUpWhenGetMethodWithForm() {
+    try {
+      new RestOperation("blah", "GET", singletonMap("form", emptyMap()));
+      expectFailing(IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+    }
+  }
+
+  @Test
+  public void blowsUpWhenGetMethodWithJson() {
+    try {
+      new RestOperation("blah", "GET", singletonMap("json", emptyMap()));
+      expectFailing(IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+    }
+  }
+
+  @Test
+  public void blowsUpWhenDeleteMethodWithForm() {
+    try {
+      new RestOperation("blah", "DELETE", singletonMap("form", emptyMap()));
+      expectFailing(IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+    }
+  }
+
+  @Test
+  public void blowsUpWhenDeleteMethodWithJson() {
+    try {
+      new RestOperation("blah", "DELETE", singletonMap("json", emptyMap()));
+      expectFailing(IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+    }
+  }
+
+  @Test
+  public void blowsUpWhenMethodIsNotSupported() {
+    try {
+      new RestOperation("blah", "foo", emptyMap());
+      expectFailing(IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), is("Unsupported method foo"));
+    }
+  }
+
+  @Test
+  public void blowsUpWhenMethodIsNull() {
+    try {
+      new RestOperation("blah", null, emptyMap());
+      expectFailing(IllegalArgumentException.class);
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage(), is("Unsupported method null"));
+    }
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/RetrySagaLogTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RetrySagaLogTest.java
new file mode 100644
index 0000000..6da8f16
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RetrySagaLogTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.apache.servicecomb.saga.core.SagaTaskFactory.RetrySagaLog;
+
+public class RetrySagaLogTest {
+
+  private final PersistentStore persistentStore = mock(PersistentStore.class);
+  private final SagaRequest sagaRequest = mock(SagaRequest.class);
+  private final SagaEvent dummyEvent = new DummyEvent(sagaRequest);
+  private final RetrySagaLog retrySagaLog = new RetrySagaLog(persistentStore, 100);
+
+  private volatile boolean interrupted = false;
+
+  @Test
+  public void retryUntilSuccessWhenEventIsNotPersisted() throws InterruptedException {
+    doThrow(RuntimeException.class).
+        doThrow(RuntimeException.class).
+        doThrow(RuntimeException.class).
+        doThrow(RuntimeException.class).
+        doThrow(RuntimeException.class).
+        doNothing().
+        when(persistentStore).offer(dummyEvent);
+
+    retrySagaLog.offer(dummyEvent);
+
+    verify(persistentStore, times(6)).offer(dummyEvent);
+  }
+
+  @Test
+  public void exitOnInterruption() throws InterruptedException {
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    Future<?> future = executor.submit(() -> {
+      doThrow(RuntimeException.class).when(persistentStore).offer(dummyEvent);
+
+      retrySagaLog.offer(dummyEvent);
+      interrupted = true;
+    });
+
+    Thread.sleep(500);
+
+    assertThat(future.cancel(true), is(true));
+
+    await().atMost(2, TimeUnit.SECONDS).until(() -> interrupted);
+    executor.shutdown();
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEndTaskTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEndTaskTest.java
new file mode 100644
index 0000000..22f5fa1
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEndTaskTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class SagaEndTaskTest {
+  private final SagaRequest request = mock(SagaRequest.class);
+  private final SagaLog sagaLog = mock(SagaLog.class);
+
+  private final String sagaId = "0";
+  private final SagaEndTask sagaEndTask = new SagaEndTask(sagaId, sagaLog);
+
+  @Test
+  public void emptyResponseOnSuccessfulEventPersistence() throws Exception {
+    ArgumentCaptor<SagaEndedEvent> argumentCaptor = ArgumentCaptor.forClass(SagaEndedEvent.class);
+    doNothing().when(sagaLog).offer(argumentCaptor.capture());
+
+    sagaEndTask.commit(request, SagaResponse.EMPTY_RESPONSE);
+
+    SagaEndedEvent event = argumentCaptor.getValue();
+    assertThat(event.sagaId, is(sagaId));
+    assertThat(event.json(null), is("{}"));
+    assertThat(event.payload(), is(request));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEventMatcher.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEventMatcher.java
new file mode 100644
index 0000000..7e8289c
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEventMatcher.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.Objects;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class SagaEventMatcher extends TypeSafeMatcher<SagaEvent> {
+
+  private final String sagaId;
+  private final Operation operation;
+  private final Class<?> aClass;
+
+  public static Matcher<SagaEvent> eventWith(String sagaId, Operation operation, Class<?> aClass) {
+    return new SagaEventMatcher(sagaId, operation, aClass);
+  }
+
+  static Matcher<SagaEvent> eventWith(Operation operation, Class<?> aClass) {
+    return eventWith("0", operation, aClass);
+  }
+
+  private SagaEventMatcher(String sagaId, Operation operation, Class<?> aClass) {
+    this.sagaId = sagaId;
+    this.operation = operation;
+    this.aClass = aClass;
+  }
+
+  @Override
+  protected void describeMismatchSafely(SagaEvent item, Description description) {
+    description
+        .appendText("SagaEvent {sagaId=" + item.sagaId + ", operation=" + operation(item) + ", class=" + item.getClass());
+  }
+
+  @Override
+  protected boolean matchesSafely(SagaEvent envelope) {
+    return Objects.equals(envelope.sagaId, sagaId)
+        && operation(envelope).equals(operation)
+        && envelope.getClass().equals(aClass);
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description
+        .appendText("SagaEvent {sagaId=" + sagaId + ", operation=" + operation + ", class=" + aClass.getCanonicalName());
+  }
+
+  private Operation operation(SagaEvent envelope) {
+    return operation instanceof Compensation ? envelope.payload().compensation() : envelope.payload().transaction();
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaExecutionComponentTestBase.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaExecutionComponentTestBase.java
new file mode 100644
index 0000000..b9e893f
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaExecutionComponentTestBase.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static org.apache.servicecomb.saga.core.Operation.TYPE_REST;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.waitAtMost;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.application.SagaExecutionComponent;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+
+@SuppressWarnings("unchecked")
+public abstract class SagaExecutionComponentTestBase {
+  private static final String requestJson = "[\n"
+      + "  {\n"
+      + "    \"id\": \"request-1\",\n"
+      + "    \"serviceName\": \"aaa\",\n"
+      + "    \"transaction\": {\n"
+      + "      \"method\": \"post\",\n"
+      + "      \"path\": \"/rest/as\"\n"
+      + "    },\n"
+      + "    \"compensation\": {\n"
+      + "      \"method\": \"delete\",\n"
+      + "      \"path\": \"/rest/as\"\n"
+      + "    }\n"
+      + "  }\n"
+      + "]\n";
+
+  private static final String sagaJson = "{\n"
+      + "  \"policy\": \"ForwardRecovery\",\n"
+      + "  \"requests\": " + requestJson + "\n"
+      + "}";
+
+  private static final String anotherRequestJson = "[\n"
+      + "  {\n"
+      + "    \"id\": \"request-2\",\n"
+      + "    \"serviceName\": \"bbb\",\n"
+      + "    \"transaction\": {\n"
+      + "      \"method\": \"post\",\n"
+      + "      \"path\": \"/rest/bs\"\n"
+      + "    },\n"
+      + "    \"compensation\": {\n"
+      + "      \"method\": \"delete\",\n"
+      + "      \"path\": \"/rest/bs\"\n"
+      + "    }\n"
+      + "  }\n"
+      + "]\n";
+
+  private static final String anotherSagaJson = "{\n"
+      + "  \"policy\": \"ForwardRecovery\",\n"
+      + "  \"requests\": " + anotherRequestJson + "\n"
+      + "}";
+
+  private final SagaRequest request1 = new SagaRequestImpl(
+      "request-1",
+      "aaa",
+      TYPE_REST,
+      new TransactionImpl("/rest/as", "post", emptyMap()),
+      new CompensationImpl("/rest/as", "delete", emptyMap())
+  );
+
+  private final SagaRequest request2 = new SagaRequestImpl(
+      "request-2",
+      "bbb",
+      TYPE_REST,
+      new TransactionImpl("/rest/bs", "post", emptyMap()),
+      new CompensationImpl("/rest/bs", "delete", emptyMap())
+  );
+
+  private final SagaDefinition definition1 = new SagaDefinition() {
+    @Override
+    public RecoveryPolicy policy() {
+      return new ForwardRecovery();
+    }
+
+    @Override
+    public SagaRequest[] requests() {
+      return new SagaRequest[]{request1};
+    }
+  };
+
+  private final SagaDefinition definition2 = new SagaDefinition() {
+    @Override
+    public RecoveryPolicy policy() {
+      return new BackwardRecovery();
+    }
+
+    @Override
+    public SagaRequest[] requests() {
+      return new SagaRequest[]{request2};
+    }
+  };
+
+  private final FromJsonFormat<SagaDefinition> fromJsonFormat = Mockito.mock(FromJsonFormat.class);
+  private final EmbeddedPersistentStore eventStore = new EmbeddedPersistentStore();
+
+  private final SagaExecutionComponent coordinator = new SagaExecutionComponent(
+      eventStore,
+      fromJsonFormat,
+      null,
+      sagaFactory(eventStore)
+  );
+
+  private final String sagaId = "1";
+
+  @Before
+  public void setUp() throws Exception {
+    when(fromJsonFormat.fromJson(sagaJson)).thenReturn(definition1);
+    when(fromJsonFormat.fromJson(anotherSagaJson)).thenReturn(definition2);
+  }
+
+  @Test
+  public void recoverSagaWithEventsFromEventStore() throws IOException {
+    eventStore.offer(new SagaStartedEvent(sagaId, sagaJson, NoOpSagaRequest.SAGA_START_REQUEST));
+    coordinator.reanimate();
+
+    assertThat(eventStore, contains(
+        eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+        eventWith(request1, TransactionStartedEvent.class),
+        eventWith(request1, TransactionEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+    ));
+  }
+
+  @Test
+  public void runSagaWithEventStore() throws IOException {
+    SagaResponse response = coordinator.run(sagaJson);
+
+    assertThat(response, is(SagaResponse.EMPTY_RESPONSE));
+    assertThat(eventStore, contains(
+        eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+        eventWith(request1, TransactionStartedEvent.class),
+        eventWith(request1, TransactionEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+    ));
+  }
+
+  @Test
+  public void processRequestsInParallel() {
+    CompletableFuture.runAsync(() -> coordinator.run(sagaJson));
+    CompletableFuture.runAsync(() -> coordinator.run(anotherSagaJson));
+
+    waitAtMost(2, SECONDS).until(() -> eventStore.size() == 8);
+
+    assertThat(eventStore, containsInAnyOrder(
+        eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+        eventWith(request1, TransactionStartedEvent.class),
+        eventWith(request1, TransactionEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+        eventWith(request2, TransactionStartedEvent.class),
+        eventWith(request2, TransactionEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+    ));
+  }
+
+  @Test
+  public void runSagaAfterRecovery() throws IOException {
+    eventStore.offer(new SagaStartedEvent(sagaId, sagaJson, NoOpSagaRequest.SAGA_START_REQUEST));
+    coordinator.reanimate();
+
+    coordinator.run(anotherSagaJson);
+
+    assertThat(eventStore, contains(
+        eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+        eventWith(request1, TransactionStartedEvent.class),
+        eventWith(request1, TransactionEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+        eventWith(request2, TransactionStartedEvent.class),
+        eventWith(request2, TransactionEndedEvent.class),
+        eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+    ));
+  }
+
+  private Matcher<SagaEvent> eventWith(
+      SagaRequest sagaRequest,
+      Class<?> type) {
+
+    return new TypeSafeMatcher<SagaEvent>() {
+      @Override
+      protected boolean matchesSafely(SagaEvent event) {
+        SagaRequest request = event.payload();
+        return sagaRequest.equals(request)
+            && event.getClass().equals(type);
+      }
+
+      @Override
+      protected void describeMismatchSafely(SagaEvent item, Description mismatchDescription) {
+        SagaRequest request = item.payload();
+        mismatchDescription.appendText(
+            "SagaEvent {" + request + "}");
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(
+            "SagaEvent {" + sagaRequest + "}");
+      }
+    };
+  }
+
+  private class EmbeddedPersistentStore extends EmbeddedEventStore implements PersistentStore {
+
+    @Override
+    public Map<String, List<EventEnvelope>> findPendingSagaEvents() {
+      return singletonMap(sagaId, singletonList(
+          new EventEnvelope(1L, new SagaStartedEvent(sagaId, sagaJson, NoOpSagaRequest.SAGA_START_REQUEST))));
+    }
+  }
+
+  protected abstract SagaFactory sagaFactory(PersistentStore eventStore);
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaIntegrationTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaIntegrationTest.java
new file mode 100644
index 0000000..d005ee7
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaIntegrationTest.java
@@ -0,0 +1,687 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.apache.servicecomb.saga.core.Compensation.SAGA_START_COMPENSATION;
+import static org.apache.servicecomb.saga.core.Operation.TYPE_REST;
+import static org.apache.servicecomb.saga.core.SagaEventMatcher.eventWith;
+import static org.apache.servicecomb.saga.core.SagaResponse.EMPTY_RESPONSE;
+import static org.apache.servicecomb.saga.core.SagaResponse.NONE_RESPONSE;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_END_TASK;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_REQUEST_TASK;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_START_TASK;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_END_TRANSACTION;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_START_TRANSACTION;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.stubbing.Answer;
+
+import com.seanyinx.github.unit.scaffolding.Randomness;
+
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.core.dag.Node;
+import org.apache.servicecomb.saga.core.dag.SingleLeafDirectedAcyclicGraph;
+import org.apache.servicecomb.saga.infrastructure.ContextAwareEventStore;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+
+@SuppressWarnings("unchecked")
+public class SagaIntegrationTest {
+  private static final String sagaId = Randomness.uniquify("sagaId");
+
+  private final FromJsonFormat<Set<String>> childrenExtractor = mock(FromJsonFormat.class);
+  private final SagaContext sagaContext = new SagaContextImpl(childrenExtractor);
+  private final IdGenerator<Long> idGenerator = new LongIdGenerator();
+  private final EventStore eventStore = new EmbeddedEventStore();
+  private final ContextAwareEventStore sagaLog = new ContextAwareEventStore(eventStore, sagaContext);
+
+  private final Transaction transaction1 = mock(Transaction.class, "transaction1");
+  private final Transaction transaction2 = mock(Transaction.class, "transaction2");
+  private final Transaction transaction3 = mock(Transaction.class, "transaction3");
+  private final Transaction transaction4 = mock(Transaction.class, "transaction4");
+
+  private final Compensation compensation1 = mock(Compensation.class, "compensation1");
+  private final Compensation compensation2 = mock(Compensation.class, "compensation2");
+  private final Compensation compensation3 = mock(Compensation.class, "compensation3");
+  private final Compensation compensation4 = mock(Compensation.class, "compensation4");
+
+  private final Fallback fallback1 = mock(Fallback.class, "fallback1");
+
+  private final String requestJson = "{}";
+  private final SagaRequest request1 = request("request1", "service1", transaction1, compensation1, fallback1);
+  private final SagaRequest request2 = request("request2", "service2", transaction2, compensation2, request1.id());
+  private final SagaRequest request3 = request("request3", "service3", transaction3, compensation3, request1.id());
+  private final SagaRequest request4 = request("request4", "service4", transaction4, compensation4, request3.id());
+
+  private final SagaResponse transactionResponse1 = new SuccessfulSagaResponse("transaction1");
+  private final SagaResponse transactionResponse2 = new SuccessfulSagaResponse("transaction2");
+  private final SagaResponse transactionResponse3 = new SuccessfulSagaResponse("transaction3");
+  private final SagaResponse compensationResponse1 = new SuccessfulSagaResponse("compensation1");
+  private final SagaResponse compensationResponse2 = new SuccessfulSagaResponse("compensation2");
+  private final SagaResponse compensationResponse3 = new SuccessfulSagaResponse("compensation3");
+
+  @SuppressWarnings("ThrowableInstanceNeverThrown")
+  private final RuntimeException exception = new RuntimeException("oops");
+
+  private final Node<SagaRequest> node1 = new Node<>(1, request1);
+  private final Node<SagaRequest> node2 = new Node<>(2, request2);
+  private final Node<SagaRequest> node3 = new Node<>(3, request3);
+  private final Node<SagaRequest> node4 = new Node<>(4, request4);
+  private final Node<SagaRequest> root = new Node<>(0, NoOpSagaRequest.SAGA_START_REQUEST);
+  private final Node<SagaRequest> leaf = new Node<>(5, NoOpSagaRequest.SAGA_END_REQUEST);
+  private final SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph = new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+
+  private Saga saga;
+  private final Map<String, SagaTask> tasks = new HashMap<>();
+
+  // root - node1 - node2 - leaf
+  @Before
+  public void setUp() throws Exception {
+    when(childrenExtractor.fromJson(anyString())).thenReturn(emptySet());
+    when(childrenExtractor.fromJson(NONE_RESPONSE.body())).thenReturn(setOf("none"));
+
+    when(transaction1.send(request1.serviceName(), EMPTY_RESPONSE)).thenReturn(transactionResponse1);
+    when(transaction2.send(request2.serviceName(), transactionResponse1)).thenReturn(transactionResponse2);
+    when(transaction3.send(request3.serviceName(), transactionResponse1)).thenReturn(transactionResponse3);
+
+    when(compensation1.send(request1.serviceName(), compensationResponse2)).thenReturn(compensationResponse1);
+    when(compensation2.send(request2.serviceName(), compensationResponse3)).thenReturn(compensationResponse2);
+    when(compensation3.send(request3.serviceName(), EMPTY_RESPONSE)).thenReturn(compensationResponse3);
+
+    root.addChild(node1);
+    node1.addChild(node2);
+    node2.addChild(leaf);
+
+    SagaStartTask sagaStartTask = new SagaStartTask(sagaId, requestJson, sagaLog);
+    SagaEndTask sagaEndTask = new SagaEndTask(sagaId, sagaLog);
+    RequestProcessTask processTask = requestProcessTask(new BackwardRecovery());
+
+    tasks.put(SAGA_START_TASK, sagaStartTask);
+    tasks.put(SAGA_REQUEST_TASK, processTask);
+    tasks.put(SAGA_END_TASK, sagaEndTask);
+
+    saga = new GraphBasedSaga(eventStore, tasks, sagaContext, sagaTaskGraph);
+  }
+
+  @Test
+  public void transactionsAreRunSuccessfully() {
+    saga.run();
+
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+  }
+
+  // root - node1 - node2 - leaf
+  //             \_ node3 _/
+  @Test
+  public void compensateCommittedTransactionsOnFailure() {
+    addExtraChildToNode1();
+
+    // barrier to make sure the two transactions starts at the same time
+    CyclicBarrier barrier = new CyclicBarrier(2);
+    when(transaction2.send(request2.serviceName(), transactionResponse1))
+        .thenAnswer(
+            withAnswer(() -> {
+              barrier.await();
+              Thread.sleep(100);
+              throw exception;
+            }));
+
+    when(transaction3.send(request3.serviceName(), transactionResponse1))
+        .thenAnswer(
+            withAnswer(() -> {
+              barrier.await();
+              return transactionResponse3;
+            }));
+
+    saga.run();
+
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        anyOf(eventWith(sagaId, transaction2, TransactionStartedEvent.class), eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+        anyOf(eventWith(sagaId, transaction2, TransactionStartedEvent.class), eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+        eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionAbortedEvent.class),
+        eventWith(sagaId, compensation3, TransactionCompensatedEvent.class),
+        eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction2).send(request2.serviceName(), transactionResponse1);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3).send(request3.serviceName());
+  }
+
+  @Test
+  public void skipIgnoredTransaction() throws Exception {
+    addExtraChildToNode1();
+
+    when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+    saga.run();
+
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void skipAllIgnoredTransactions() throws Exception {
+    node1.addChild(node3);
+    node3.addChild(node4);
+    node4.addChild(leaf);
+
+    when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf("none"));
+
+    saga.run();
+
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction4, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+    verify(compensation4, never()).send(request4.serviceName());
+  }
+
+  @Test
+  public void doNotCompensateIgnoredTransactions() throws Exception {
+    node1.addChild(node3);
+    node3.addChild(node4);
+    node4.addChild(leaf);
+
+    when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+    when(transaction4.send(request4.serviceName(), transactionResponse3)).thenThrow(exception);
+
+    saga.run();
+
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction4, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction4, TransactionAbortedEvent.class),
+        eventWith(sagaId, transaction3, TransactionCompensatedEvent.class),
+        eventWith(sagaId, transaction1, TransactionCompensatedEvent.class),
+        eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+    verify(transaction4).send(request4.serviceName(), transactionResponse3);
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation3).send(request3.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation4, never()).send(request4.serviceName());
+  }
+
+  // root - node1 - node2 - leaf
+  //             \_ node3 _/
+  @Test
+  public void redoHangingTransactionsOnFailure() throws InterruptedException {
+    addExtraChildToNode1();
+
+    // barrier to make sure the two transactions starts at the same time
+    CyclicBarrier barrier = new CyclicBarrier(2);
+    when(transaction3.send(request3.serviceName(), transactionResponse1))
+        .thenAnswer(withAnswer(() -> {
+      barrier.await();
+      throw exception;
+    }));
+
+    CountDownLatch latch = new CountDownLatch(1);
+
+    when(transaction2.send(request2.serviceName(), transactionResponse1))
+        .thenAnswer(withAnswer(() -> {
+      barrier.await();
+      latch.await();
+      return transactionResponse2;
+    })).thenReturn(transactionResponse2);
+
+    saga.run();
+
+    // the ordering of events may not be consistence due to concurrent processing of requests
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        anyOf(
+            eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+            eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+        anyOf(
+            eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+            eventWith(sagaId, transaction2, TransactionStartedEvent.class)),
+        eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction2, times(2)).send(request2.serviceName(), transactionResponse1);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+
+    latch.countDown();
+  }
+
+  @Test
+  public void retriesFailedTransactionTillSuccess() {
+    RequestProcessTask processTask = requestProcessTask(new ForwardRecovery());
+    tasks.put(SAGA_REQUEST_TASK, processTask);
+
+    when(transaction2.send(request2.serviceName(), transactionResponse1))
+        .thenThrow(exception).thenThrow(exception).thenReturn(transactionResponse2);
+
+    saga.run();
+
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction2, times(3)).send(request2.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(anyString(), any(SagaResponse.class));
+    verify(compensation2, never()).send(anyString(), any(SagaResponse.class));
+  }
+
+  @Test
+  public void fallbackWhenCompensationFailed() {
+    int retries = 3;
+
+    when(transaction2.send(request2.serviceName(), transactionResponse1)).thenThrow(exception);
+    when(compensation1.send(request1.serviceName())).thenThrow(exception);
+    when(compensation1.retries()).thenReturn(retries);
+
+    saga.run();
+
+    verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+    verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+    verify(compensation1, times(retries + 1)).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+
+    verify(fallback1).send(request1.serviceName());
+  }
+
+  @Test
+  public void restoresSagaToTransactionStateByPlayingAllEvents() {
+    addExtraChildToNode1();
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresPartialTransactionByPlayingAllEvents() {
+    addExtraChildToNode1();
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2)),
+        envelope(new TransactionStartedEvent(sagaId, request3))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresToCompensationFromAbortedTransactionByPlayingAllEvents() {
+    addExtraChildToNode1();
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2)),
+        envelope(new TransactionStartedEvent(sagaId, request3)),
+        envelope(new TransactionAbortedEvent(sagaId, request3, exception))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresSagaToCompensationStateByPlayingAllEvents() {
+    addExtraChildToNode1();
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2)),
+        envelope(new TransactionStartedEvent(sagaId, request3)),
+        envelope(new TransactionEndedEvent(sagaId, request3)),
+        envelope(new TransactionCompensatedEvent(sagaId, request2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        eventWith(sagaId, compensation3, TransactionCompensatedEvent.class),
+        eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresPartialCompensationByPlayingAllEvents() {
+    addExtraChildToNode1();
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2)),
+        envelope(new TransactionStartedEvent(sagaId, request3)),
+        envelope(new TransactionAbortedEvent(sagaId, request3, exception)),
+        envelope(new TransactionCompensatedEvent(sagaId, request2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresSagaToEndStateByPlayingAllEvents() {
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, contains(
+        eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+  }
+
+  @Test
+  public void failFastIfSagaLogIsDown() throws Exception {
+    SagaLog sagaLog = mock(SagaLog.class);
+    tasks.put(SAGA_START_TASK, new SagaStartTask(sagaId, requestJson, sagaLog));
+
+    doThrow(RuntimeException.class).when(sagaLog).offer(any(SagaStartedEvent.class));
+
+    try {
+      saga.run();
+      expectFailing(SagaStartFailedException.class);
+    } catch (SagaStartFailedException e) {
+      assertThat(e.getMessage(), is("Failed to persist SagaStartedEvent for " + requestJson));
+    }
+
+    verify(sagaLog).offer(any(SagaStartedEvent.class));
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+  }
+
+  private Answer<SagaResponse> withAnswer(Callable<SagaResponse> callable) {
+    return invocationOnMock -> callable.call();
+  }
+
+  private EventEnvelope envelope(SagaEvent event) {
+    return new EventEnvelope(idGenerator.nextId(), event);
+  }
+
+  private void addExtraChildToNode1() {
+    node1.addChild(node3);
+    node3.addChild(leaf);
+  }
+
+  private SagaRequest request(String requestId,
+      String serviceName,
+      Transaction transaction,
+      Compensation compensation,
+      String... parentIds) {
+
+    return new SagaRequestImpl(requestId, serviceName, TYPE_REST, transaction, compensation, parentIds);
+  }
+
+  private SagaRequest request(String requestId,
+      String serviceName,
+      Transaction transaction,
+      Compensation compensation,
+      Fallback fallback) {
+
+    return new SagaRequestImpl(requestId, serviceName, TYPE_REST, transaction, compensation, fallback);
+  }
+
+  private HashSet<String> setOf(String requestId) {
+    return new HashSet<>(singletonList(requestId));
+  }
+
+  private RequestProcessTask requestProcessTask(RecoveryPolicy recoveryPolicy) {
+    return new RequestProcessTask(sagaId, sagaLog, recoveryPolicy, new FallbackPolicy(100));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaStartTaskTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaStartTaskTest.java
new file mode 100644
index 0000000..b1d0c4e
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaStartTaskTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class SagaStartTaskTest {
+  private final SagaRequest request = mock(SagaRequest.class);
+  private final SagaLog sagaLog = mock(SagaLog.class);
+
+  private final String sagaId = "0";
+  private final String requestJson = null;
+  private final SagaStartTask sagaStartTask = new SagaStartTask(sagaId, requestJson, sagaLog);
+
+  @Test
+  public void emptyResponseOnSuccessfulEventPersistence() throws Exception {
+    ArgumentCaptor<SagaStartedEvent> argumentCaptor = ArgumentCaptor.forClass(SagaStartedEvent.class);
+    doNothing().when(sagaLog).offer(argumentCaptor.capture());
+
+    sagaStartTask.commit(request, SagaResponse.EMPTY_RESPONSE);
+
+    SagaStartedEvent event = argumentCaptor.getValue();
+    assertThat(event.sagaId, is(sagaId));
+    assertThat(event.json(null), is(requestJson));
+    assertThat(event.payload(), is(request));
+  }
+
+  @Test
+  public void blowsUpWhenEventIsNotPersisted() {
+    doThrow(RuntimeException.class).when(sagaLog).offer(any(SagaStartedEvent.class));
+
+    try {
+      sagaStartTask.commit(request, SagaResponse.EMPTY_RESPONSE);
+      expectFailing(SagaStartFailedException.class);
+    } catch (SagaStartFailedException e) {
+      assertThat(e.getMessage(), startsWith("Failed to persist SagaStartedEvent"));
+    }
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/TransactionImpl.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/TransactionImpl.java
new file mode 100644
index 0000000..a59b142
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/TransactionImpl.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core;
+
+import java.util.Map;
+
+public class TransactionImpl extends RestOperation implements Transaction {
+
+  public TransactionImpl(String path, String method, Map<String, Map<String, String>> params) {
+    super(path, method, params);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaExecutionComponentTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaExecutionComponentTest.java
new file mode 100644
index 0000000..01f8adb
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaExecutionComponentTest.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaExecutionComponentTestBase;
+
+
+public class ActorBasedSagaExecutionComponentTest extends SagaExecutionComponentTestBase {
+
+  @Override
+  protected SagaFactory sagaFactory(PersistentStore eventStore) {
+    return new ActorBasedSagaFactory(500, eventStore, null);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaIntegrationTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaIntegrationTest.java
new file mode 100644
index 0000000..ac649f0
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaIntegrationTest.java
@@ -0,0 +1,691 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_END_TRANSACTION;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_START_TRANSACTION;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+
+import org.apache.servicecomb.saga.core.Compensation;
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.Fallback;
+import org.apache.servicecomb.saga.core.ForwardRecovery;
+import org.apache.servicecomb.saga.core.IdGenerator;
+import org.apache.servicecomb.saga.core.LongIdGenerator;
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.Operation;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaEventMatcher;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequestImpl;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaStartedEvent;
+import org.apache.servicecomb.saga.core.SuccessfulSagaResponse;
+import org.apache.servicecomb.saga.core.TransactionCompensatedEvent;
+import org.apache.servicecomb.saga.core.TransactionStartedEvent;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.stubbing.Answer;
+
+import com.seanyinx.github.unit.scaffolding.Randomness;
+
+import org.apache.servicecomb.saga.core.BackwardRecovery;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaEndedEvent;
+import org.apache.servicecomb.saga.core.SagaEvent;
+import org.apache.servicecomb.saga.core.Transaction;
+import org.apache.servicecomb.saga.core.TransactionAbortedEvent;
+import org.apache.servicecomb.saga.core.TransactionEndedEvent;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+
+@SuppressWarnings("unchecked")
+public class ActorBasedSagaIntegrationTest {
+  private static final String sagaId = Randomness.uniquify("sagaId");
+
+  private final FromJsonFormat<Set<String>> childrenExtractor = mock(FromJsonFormat.class);
+  private final IdGenerator<Long> idGenerator = new LongIdGenerator();
+  private final EventStore eventStore = new EmbeddedEventStore();
+
+  private final PersistentStore persistentStore = mock(PersistentStore.class);
+  private final SagaDefinition sagaDefinition = mock(SagaDefinition.class);
+
+  private final Transaction transaction1 = mock(Transaction.class, "transaction1");
+  private final Transaction transaction2 = mock(Transaction.class, "transaction2");
+  private final Transaction transaction3 = mock(Transaction.class, "transaction3");
+  private final Transaction transaction4 = mock(Transaction.class, "transaction4");
+
+  private final Compensation compensation1 = mock(Compensation.class, "compensation1");
+  private final Compensation compensation2 = mock(Compensation.class, "compensation2");
+  private final Compensation compensation3 = mock(Compensation.class, "compensation3");
+  private final Compensation compensation4 = mock(Compensation.class, "compensation4");
+
+  private final Fallback fallback1 = mock(Fallback.class, "fallback1");
+
+  private final String requestJson = "{}";
+  private final SagaRequest request1 = request("request1", "service1", transaction1, compensation1, fallback1);
+  private final SagaRequest request2 = request("request2", "service2", transaction2, compensation2, request1.id());
+  private final SagaRequest request3 = request("request3", "service3", transaction3, compensation3, request1.id());
+  private final SagaRequest request4 = request("request4", "service4", transaction4, compensation4, request3.id());
+
+  private final SagaResponse transactionResponse1 = new SuccessfulSagaResponse("transaction1");
+  private final SagaResponse transactionResponse2 = new SuccessfulSagaResponse("transaction2");
+  private final SagaResponse transactionResponse3 = new SuccessfulSagaResponse("transaction3");
+  private final SagaResponse compensationResponse1 = new SuccessfulSagaResponse("compensation1");
+  private final SagaResponse compensationResponse2 = new SuccessfulSagaResponse("compensation2");
+  private final SagaResponse compensationResponse3 = new SuccessfulSagaResponse("compensation3");
+
+  @SuppressWarnings("ThrowableInstanceNeverThrown")
+  private final RuntimeException exception = new RuntimeException("oops");
+
+  private Saga saga;
+  private final SagaFactory sagaFactory = new ActorBasedSagaFactory(100, persistentStore, childrenExtractor);
+
+  // root - node1 - node2 - leaf
+  @Before
+  public void setUp() throws Exception {
+    when(sagaDefinition.policy()).thenReturn(new BackwardRecovery());
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2});
+
+    when(childrenExtractor.fromJson(anyString())).thenReturn(emptySet());
+    when(childrenExtractor.fromJson(SagaResponse.NONE_RESPONSE.body())).thenReturn(setOf("none"));
+
+    when(transaction1.send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE)).thenReturn(transactionResponse1);
+    when(transaction2.send(request2.serviceName(), transactionResponse1)).thenReturn(transactionResponse2);
+    when(transaction3.send(request3.serviceName(), transactionResponse1)).thenReturn(transactionResponse3);
+
+    when(compensation1.send(request1.serviceName(), compensationResponse2)).thenReturn(compensationResponse1);
+    when(compensation2.send(request2.serviceName(), compensationResponse3)).thenReturn(compensationResponse2);
+    when(compensation3.send(request3.serviceName(), SagaResponse.EMPTY_RESPONSE)).thenReturn(compensationResponse3);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    sagaFactory.terminate();
+    assertTrue(sagaFactory.isTerminated());
+  }
+
+  @Test
+  public void transactionsAreRunSuccessfully() {
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+    saga.run();
+
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+  }
+
+  // root - node1 - node2 - leaf
+  //             \_ node3 _/
+  @Test
+  public void compensateCommittedTransactionsOnFailure() {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    // barrier to make sure the two transactions starts at the same time
+    CyclicBarrier barrier = new CyclicBarrier(2);
+    when(transaction2.send(request2.serviceName(), transactionResponse1))
+        .thenAnswer(
+            withAnswer(() -> {
+              barrier.await();
+              Thread.sleep(100);
+              throw exception;
+            }));
+
+    when(transaction3.send(request3.serviceName(), transactionResponse1))
+        .thenAnswer(
+            withAnswer(() -> {
+              barrier.await();
+              return transactionResponse3;
+            }));
+
+    saga.run();
+
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        CoreMatchers.anyOf(SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class), SagaEventMatcher
+            .eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+        CoreMatchers.anyOf(SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class), SagaEventMatcher
+            .eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionAbortedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation3, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction2).send(request2.serviceName(), transactionResponse1);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3).send(request3.serviceName());
+  }
+
+  @Test
+  public void skipIgnoredTransaction() throws Exception {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+    saga.run();
+
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void skipAllIgnoredTransactions() throws Exception {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3, request4});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf("none"));
+
+    saga.run();
+
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction4, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+    verify(compensation4, never()).send(request4.serviceName());
+  }
+
+  @Test
+  public void doNotCompensateIgnoredTransactions() throws Exception {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3, request4});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+    when(transaction4.send(request4.serviceName(), transactionResponse3)).thenThrow(exception);
+
+    saga.run();
+
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction4, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction4, TransactionAbortedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+    verify(transaction4).send(request4.serviceName(), transactionResponse3);
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation3).send(request3.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation4, never()).send(request4.serviceName());
+  }
+
+  // TODO: 2017/10/31 actor will hang and its parent and children will be blocked without its response, timeout must be applied
+  @Ignore
+  // root - node1 - node2 - leaf
+  //             \_ node3 _/
+  @Test
+  public void redoHangingTransactionsOnFailure() throws InterruptedException {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    // barrier to make sure the two transactions starts at the same time
+    CyclicBarrier barrier = new CyclicBarrier(2);
+    when(transaction3.send(request3.serviceName(), transactionResponse1))
+        .thenAnswer(withAnswer(() -> {
+      barrier.await();
+      throw exception;
+    }));
+
+    CountDownLatch latch = new CountDownLatch(1);
+
+    when(transaction2.send(request2.serviceName(), transactionResponse1))
+        .thenAnswer(withAnswer(() -> {
+      barrier.await();
+      latch.await(1, SECONDS);
+      return transactionResponse2;
+    })).thenReturn(transactionResponse2);
+
+    saga.run();
+
+    // the ordering of events may not be consistence due to concurrent processing of requests
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        CoreMatchers.anyOf(
+            SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+            SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+        CoreMatchers.anyOf(
+            SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+            SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class)),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction2, times(2)).send(request2.serviceName(), transactionResponse1);
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+
+    latch.countDown();
+  }
+
+  @Test
+  public void retriesFailedTransactionTillSuccess() {
+    when(sagaDefinition.policy()).thenReturn(new ForwardRecovery());
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    when(transaction2.send(request2.serviceName(), transactionResponse1))
+        .thenThrow(exception).thenThrow(exception).thenReturn(transactionResponse2);
+
+    saga.run();
+
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction2, times(3)).send(request2.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(anyString(), any(SagaResponse.class));
+    verify(compensation2, never()).send(anyString(), any(SagaResponse.class));
+  }
+
+  @Test
+  public void fallbackWhenCompensationFailed() {
+    int retries = 3;
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    when(transaction2.send(request2.serviceName(), transactionResponse1)).thenThrow(exception);
+    when(compensation1.send(request1.serviceName())).thenThrow(exception);
+    when(compensation1.retries()).thenReturn(retries);
+
+    saga.run();
+
+    verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+    verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+    verify(compensation1, times(retries + 1)).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+
+    verify(fallback1).send(request1.serviceName());
+  }
+
+  @Test
+  public void restoresSagaToTransactionStateByPlayingAllEvents() {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresPartialTransactionByPlayingAllEvents() {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2)),
+        envelope(new TransactionStartedEvent(sagaId, request3))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresToCompensationFromAbortedTransactionByPlayingAllEvents() {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2)),
+        envelope(new TransactionStartedEvent(sagaId, request3)),
+        envelope(new TransactionAbortedEvent(sagaId, request3, exception))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresSagaToCompensationStateByPlayingAllEvents() {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2)),
+        envelope(new TransactionStartedEvent(sagaId, request3)),
+        envelope(new TransactionAbortedEvent(sagaId, request3, exception)),
+        envelope(new TransactionCompensatedEvent(sagaId, request2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresPartialCompensationByPlayingAllEvents() {
+    when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2)),
+        envelope(new TransactionStartedEvent(sagaId, request3)),
+        envelope(new TransactionAbortedEvent(sagaId, request3, exception)),
+        envelope(new TransactionCompensatedEvent(sagaId, request2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+    verify(compensation3, never()).send(request3.serviceName());
+  }
+
+  @Test
+  public void restoresSagaToEndStateByPlayingAllEvents() {
+    saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+    Iterable<EventEnvelope> events = asList(
+        envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+        envelope(new TransactionStartedEvent(sagaId, request1)),
+        envelope(new TransactionEndedEvent(sagaId, request1)),
+        envelope(new TransactionStartedEvent(sagaId, request2)),
+        envelope(new TransactionEndedEvent(sagaId, request2))
+    );
+
+    eventStore.populate(events);
+    saga.play();
+
+    saga.run();
+    assertThat(eventStore, IsIterableContainingInOrder.contains(
+        SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+        SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+    ));
+
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+  }
+
+  @Test
+  public void failFastIfSagaLogIsDown() throws Exception {
+    EventStore sagaLog = mock(EventStore.class);
+    saga = sagaFactory.createSaga(requestJson, sagaId, sagaLog, sagaDefinition);
+
+    doThrow(RuntimeException.class).when(sagaLog).offer(any(SagaStartedEvent.class));
+
+    saga.run();
+
+    verify(sagaLog).offer(any(SagaStartedEvent.class));
+    verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+    verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+    verify(compensation1, never()).send(request1.serviceName());
+    verify(compensation2, never()).send(request2.serviceName());
+  }
+
+  private Answer<SagaResponse> withAnswer(Callable<SagaResponse> callable) {
+    return invocationOnMock -> callable.call();
+  }
+
+  private EventEnvelope envelope(SagaEvent event) {
+    return new EventEnvelope(idGenerator.nextId(), event);
+  }
+
+  private SagaRequest request(String requestId,
+      String serviceName,
+      Transaction transaction,
+      Compensation compensation,
+      String... parentIds) {
+
+    return new SagaRequestImpl(requestId, serviceName, Operation.TYPE_REST, transaction, compensation, parentIds);
+  }
+
+  private SagaRequest request(String requestId,
+      String serviceName,
+      Transaction transaction,
+      Compensation compensation,
+      Fallback fallback) {
+
+    return new SagaRequestImpl(requestId, serviceName, Operation.TYPE_REST, transaction, compensation, fallback);
+  }
+
+  private HashSet<String> setOf(String requestId) {
+    return new HashSet<>(singletonList(requestId));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActorTest.java
new file mode 100644
index 0000000..5823a06
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActorTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+import static akka.actor.Props.empty;
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+public class CompletionCallbackActorTest {
+  private static final ActorSystem actorSystem = ActorSystem.create();
+
+  private final SagaResponse response = Mockito.mock(SagaResponse.class);
+  private final RequestActorContext context = new RequestActorContext(null);
+
+  private final ActorRef actor1 = someActor();
+  private final ActorRef actor2 = someActor();
+
+  @Before
+  public void setUp() throws Exception {
+    context.addActor(uniquify("requestId"), actor1);
+    context.addActor(uniquify("requestId"), actor2);
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    TestKit.shutdownActorSystem(actorSystem);
+  }
+
+  @Test
+  public void killAllOnTransactionComplete() throws Exception {
+    new TestKit(actorSystem) {{
+      CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+      ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+      actor.tell(context, noSender());
+      actor.tell(new TransactMessage(null, response), noSender());
+
+      await().atMost(2, TimeUnit.SECONDS)
+          .until(() -> actor1.isTerminated() && actor2.isTerminated() && actor.isTerminated());
+
+      assertThat(future.get(), is(response));
+    }};
+  }
+
+  @Test
+  public void killAllOnCompensationComplete() throws Exception {
+    new TestKit(actorSystem) {{
+      CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+      ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+      actor.tell(context, noSender());
+      actor.tell(new CompensateMessage(response), noSender());
+
+      await().atMost(2, TimeUnit.SECONDS)
+          .until(() -> actor1.isTerminated() && actor2.isTerminated() && actor.isTerminated());
+
+      assertThat(future.get(), is(response));
+    }};
+  }
+
+  @Test
+  public void killAllOnFailure() throws Exception {
+    new TestKit(actorSystem) {{
+      CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+      ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+      actor.tell(context, noSender());
+      actor.tell(new FailMessage(new RuntimeException("oops")), noSender());
+
+      await().atMost(2, TimeUnit.SECONDS)
+          .until(() -> actor1.isTerminated() && actor2.isTerminated() && actor.isTerminated());
+
+      assertThat(future.get(), is(instanceOf(FailedSagaResponse.class)));
+    }};
+  }
+
+  @Test
+  public void tellLeafToCompensateOnAbort() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addActor(NoOpSagaRequest.SAGA_END_REQUEST.id(), getRef());
+      CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+      ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+      actor.tell(context, noSender());
+      AbortMessage message = new AbortMessage(new RuntimeException("oops"));
+      actor.tell(message, noSender());
+
+      CompensateMessage compensateMessage = (CompensateMessage) receiveOne(duration("2 seconds"));
+      assertThat(compensateMessage.response(), is(message.response()));
+    }};
+  }
+
+  private ActorRef someActor() {
+    return actorSystem.actorOf(empty());
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/EventContextImplTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/EventContextImplTest.java
new file mode 100644
index 0000000..cfd3788
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/EventContextImplTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.AbortRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.scalatest.junit.JUnitSuite;
+
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+public class EventContextImplTest extends JUnitSuite {
+  private static final ActorSystem actorSystem = ActorSystem.create();
+
+  private final SagaRequest request = Mockito.mock(SagaRequest.class);
+  private final SagaResponse response = Mockito.mock(SagaResponse.class);
+
+  private final RequestActorContext context = new RequestActorContext(null);
+  private final EventContextImpl eventContext = new EventContextImpl(context);
+  private final String requestId = uniquify("requestId");
+
+  @Before
+  public void setUp() throws Exception {
+    when(request.id()).thenReturn(requestId);
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    TestKit.shutdownActorSystem(actorSystem);
+  }
+
+  @Test
+  public void sendTransactionRecoveryMessageToActor_OnTransactionEnd() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addActor(requestId, getRef());
+
+      eventContext.endTransaction(request, response);
+
+      TransactionRecoveryMessage message = (TransactionRecoveryMessage) receiveOne(duration("2 seconds"));
+      assertThat(message.response(), is(response));
+    }};
+  }
+
+  @Test
+  public void sendCompensationRecoveryMessageToActor_OnCompensationEnd() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addActor(requestId, getRef());
+
+      eventContext.compensateTransaction(request, response);
+
+      expectMsgClass(CompensationRecoveryMessage.class);
+    }};
+  }
+
+  @Test
+  public void sendAbortMessageToActor_OnAbort() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addActor(requestId, getRef());
+
+      eventContext.abortTransaction(request, response);
+
+      AbortRecoveryMessage message = ((AbortRecoveryMessage) receiveOne(duration("2 seconds")));
+      assertThat(message.response(), is(response));
+    }};
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilderTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilderTest.java
new file mode 100644
index 0000000..b3a2c14
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilderTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.hamcrest.Matchers;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.scalatest.junit.JUnitSuite;
+
+import com.seanyinx.github.unit.scaffolding.Randomness;
+
+import org.apache.servicecomb.saga.core.CompositeSagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+@SuppressWarnings("unchecked")
+public class RequestActorBuilderTest extends JUnitSuite {
+  private static final ActorSystem actorSystem = ActorSystem.create();
+
+  private final String requestId1 = Randomness.uniquify("requestId1");
+  private final String requestId2 = Randomness.uniquify("requestId2");
+  private final String requestId3 = Randomness.uniquify("requestId3");
+  private final String taskId = "some task";
+
+  private final SagaRequest request1 = Mockito.mock(SagaRequest.class);
+  private final SagaRequest request2 = Mockito.mock(SagaRequest.class);
+  private final SagaRequest request3 = Mockito.mock(SagaRequest.class);
+
+  private final SagaResponse response1 = Mockito.mock(SagaResponse.class);
+  private final SagaResponse response2 = Mockito.mock(SagaResponse.class);
+  private final SagaResponse response3 = Mockito.mock(SagaResponse.class);
+
+  private final SagaRequest[] requests = {request1, request2, request3};
+
+  private final SagaTask task = Mockito.mock(SagaTask.class);
+  private final Map<String, SagaTask> tasks = new HashMap<>();
+
+  private final FromJsonFormat<Set<String>> childrenExtractor = Mockito.mock(FromJsonFormat.class);
+  private final RequestActorBuilder actorBuilder = new RequestActorBuilder(actorSystem, childrenExtractor);
+
+  @Before
+  public void setUp() throws Exception {
+    tasks.put(SagaTask.SAGA_START_TASK, task);
+    tasks.put(SagaTask.SAGA_END_TASK, task);
+    tasks.put(taskId, task);
+
+    when(request1.id()).thenReturn(requestId1);
+    when(request2.id()).thenReturn(requestId2);
+    when(request3.id()).thenReturn(requestId3);
+
+    when(request1.task()).thenReturn(taskId);
+    when(request2.task()).thenReturn(taskId);
+    when(request3.task()).thenReturn(taskId);
+
+    when(request1.parents()).thenReturn(new String[0]);
+    when(request2.parents()).thenReturn(new String[] {requestId1});
+    when(request3.parents()).thenReturn(new String[] {requestId1});
+
+    when(task.commit(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE)).thenReturn(
+        SagaResponse.EMPTY_RESPONSE);
+    when(task.commit(request1, SagaResponse.EMPTY_RESPONSE)).thenReturn(response1);
+    when(task.commit(request2, response1)).thenReturn(response2);
+    when(task.commit(request3, response1)).thenReturn(response3);
+
+    when(childrenExtractor.fromJson(anyString())).thenReturn(Collections.emptySet());
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    TestKit.shutdownActorSystem(actorSystem);
+  }
+
+  @Test
+  public void createOneActorPerRequest() throws Exception {
+    new TestKit(actorSystem) {{
+      ArgumentCaptor<SagaResponse> argumentCaptor = ArgumentCaptor.forClass(SagaResponse.class);
+      when(task.commit(eq(NoOpSagaRequest.SAGA_END_REQUEST), argumentCaptor.capture())).thenReturn(
+          SagaResponse.EMPTY_RESPONSE);
+
+      ActorRef root = actorBuilder.build(requests, tasks, getRef()).actorOf(NoOpSagaRequest.SAGA_START_REQUEST.id());
+
+      root.tell(new TransactMessage(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE), getRef());
+
+      List<SagaResponse> responses = receiveN(1, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, Matchers.containsInAnyOrder(SagaResponse.EMPTY_RESPONSE));
+
+      verify(task).commit(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE);
+      verify(task).commit(request1, SagaResponse.EMPTY_RESPONSE);
+      verify(task).commit(request2, response1);
+      verify(task).commit(request3, response1);
+      verify(task).commit(eq(NoOpSagaRequest.SAGA_END_REQUEST), any(SagaResponse.class));
+
+      SagaResponse response = argumentCaptor.getValue();
+      assertThat(response, instanceOf(CompositeSagaResponse.class));
+      assertThat(((CompositeSagaResponse) response).responses(),
+          containsInAnyOrder(response2, response3));
+    }};
+  }
+
+  @Test
+  public void compensateAllCompletedTransactions() throws Exception {
+    new TestKit(actorSystem) {{
+      ArgumentCaptor<SagaResponse> argumentCaptor = ArgumentCaptor.forClass(SagaResponse.class);
+      when(task.commit(eq(NoOpSagaRequest.SAGA_END_REQUEST), argumentCaptor.capture())).thenReturn(
+          SagaResponse.EMPTY_RESPONSE);
+
+      ActorRef root = actorBuilder.build(requests, tasks, getRef()).actorOf(NoOpSagaRequest.SAGA_START_REQUEST.id());
+
+      root.tell(new TransactMessage(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE), getRef());
+
+      List<SagaResponse> responses = receiveN(1, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, Matchers.containsInAnyOrder(SagaResponse.EMPTY_RESPONSE));
+
+      CompensateMessage message = new CompensateMessage(response1);
+      getLastSender().tell(message, getRef());
+      expectMsg(message);
+
+      verify(task).compensate(NoOpSagaRequest.SAGA_START_REQUEST);
+      verify(task).compensate(request1);
+      verify(task).compensate(request2);
+      verify(task).compensate(request3);
+      verify(task).compensate(NoOpSagaRequest.SAGA_END_REQUEST);
+    }};
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorTest.java
new file mode 100644
index 0000000..776b5aa
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorTest.java
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+import static akka.actor.Props.empty;
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.CompositeSagaResponse;
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.Operation;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaStartFailedException;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.apache.servicecomb.saga.core.TransactionFailedException;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.hamcrest.Matchers;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.scalatest.junit.JUnitSuite;
+
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+@SuppressWarnings("unchecked")
+public class RequestActorTest extends JUnitSuite {
+  private final String parentRequestId1 = uniquify("parentRequestId1");
+  private final String parentRequestId2 = uniquify("parentRequestId2");
+  private final String requestId = uniquify("requestId");
+
+  private final SagaTask task = Mockito.mock(SagaTask.class);
+  private final SagaRequest request = Mockito.mock(SagaRequest.class, "request");
+  private final SagaRequest request1 = Mockito.mock(SagaRequest.class, "request1");
+  private final SagaRequest request2 = Mockito.mock(SagaRequest.class, "request2");
+  private final SagaResponse response = Mockito.mock(SagaResponse.class);
+  private final FromJsonFormat<Set<String>> childrenExtractor = mock(FromJsonFormat.class);
+
+  private final RequestActorContext context = new RequestActorContext(childrenExtractor);
+
+  private final TransactionFailedException exception = new TransactionFailedException("oops");
+  private static final ActorSystem actorSystem = ActorSystem.create();
+  private final CompensateMessage compensateMessage = new CompensateMessage(new FailedSagaResponse(exception));
+
+  @Before
+  public void setUp() throws Exception {
+    when(childrenExtractor.fromJson(anyString())).thenReturn(emptySet());
+    when(request.id()).thenReturn(requestId);
+
+    when(request1.id()).thenReturn(parentRequestId1);
+    when(request2.id()).thenReturn(parentRequestId2);
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    TestKit.shutdownActorSystem(actorSystem);
+  }
+
+  @Test
+  public void tellNodeResponseToAllChildren() throws Exception {
+    new TestKit(actorSystem) {{
+      addChildren(getRef());
+
+      ActorRef parent = someActor();
+      context.addParent(requestId, parent);
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+      when(task.commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE)).thenReturn(response);
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent);
+
+      List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, containsInAnyOrder(response, response));
+
+      verify(task).commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE);
+    }};
+  }
+
+  @Test
+  public void executeTransaction_OnlyWhenAllParentsResponsesAreReceived() throws Exception {
+    new TestKit(actorSystem) {{
+      addChildren(getRef());
+
+      ActorRef parent1 = someActor();
+      context.addParent(requestId, parent1);
+
+      ActorRef parent2 = someActor();
+      context.addParent(requestId, parent2);
+
+      ArgumentCaptor<SagaResponse> argumentCaptor = ArgumentCaptor.forClass(SagaResponse.class);
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1, parentRequestId2});
+      when(task.commit(eq(request), argumentCaptor.capture())).thenReturn(response);
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent1);
+      expectNoMsg(duration("500 milliseconds"));
+
+      actorRef.tell(new TransactMessage(request2, SagaResponse.EMPTY_RESPONSE), parent2);
+
+      List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, containsInAnyOrder(response, response));
+
+      SagaResponse response = argumentCaptor.getValue();
+      assertThat(response, instanceOf(CompositeSagaResponse.class));
+      assertThat(((CompositeSagaResponse) response).responses(),
+          Matchers.containsInAnyOrder(SagaResponse.EMPTY_RESPONSE, Operation.SUCCESSFUL_SAGA_RESPONSE));
+    }};
+  }
+
+  @Test
+  public void tellAllRelativesToAbortOnError() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addChild(requestId, getRef());
+      context.addActor(requestId, getRef());
+
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+      when(task.commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE)).thenThrow(exception);
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), getRef());
+
+      List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+          .map(o -> ((AbortMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, containsInAnyOrder(instanceOf(FailedSagaResponse.class), instanceOf(FailedSagaResponse.class)));
+    }};
+  }
+
+  @Test
+  public void tellAllRelativesExceptSenderToAbortOnAbort() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addChild(requestId, getRef());
+      context.addActor(requestId, getRef());
+
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new AbortMessage(exception), someActor());
+
+      List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+          .map(o -> ((AbortMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, containsInAnyOrder(instanceOf(FailedSagaResponse.class), instanceOf(FailedSagaResponse.class)));
+
+      actorRef.tell(new AbortMessage(exception), someActor());
+      expectNoMsg(duration("500 milliseconds"));
+    }};
+  }
+
+  @Test
+  public void compensateIfTransactionIsCompleted() throws Exception {
+    new TestKit(actorSystem) {{
+      addChildren(someActor());
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+      when(task.commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE)).thenReturn(response);
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), getRef());
+      actorRef.tell(new AbortMessage(exception), noSender());
+      actorRef.tell(compensateMessage, getRef());
+      actorRef.tell(compensateMessage, getRef());
+
+      expectMsg(duration("2 seconds"), compensateMessage);
+      verify(task).compensate(request);
+
+      // no duplicate compensation
+      reset(task);
+      actorRef.tell(compensateMessage, getRef());
+      actorRef.tell(compensateMessage, getRef());
+      expectNoMsg(duration("200 milliseconds"));
+      verify(task, never()).compensate(request);
+    }};
+  }
+
+  @Test
+  public void doNotCompensateIfTransactionIsNotCompleted() throws Exception {
+    new TestKit(actorSystem) {{
+      addChildren(someActor());
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new AbortMessage(exception), noSender());
+      actorRef.tell(compensateMessage, getRef());
+      actorRef.tell(compensateMessage, getRef());
+
+      List<Object> responses = receiveN(2, duration("2 seconds"));
+      assertThat(responses, contains(instanceOf(AbortMessage.class), instanceOf(CompensateMessage.class)));
+      verify(task, never()).compensate(request);
+
+      // no duplicate compensation
+      reset(task);
+      actorRef.tell(compensateMessage, getRef());
+      actorRef.tell(compensateMessage, getRef());
+      expectNoMsg(duration("200 milliseconds"));
+      verify(task, never()).compensate(request);
+    }};
+  }
+
+  @Test
+  public void skipIfActorIsNotChosenByAnyParent() throws Exception {
+    when(childrenExtractor.fromJson(Operation.SUCCESSFUL_SAGA_RESPONSE.body())).thenReturn(singleton("none"));
+
+    new TestKit(actorSystem) {{
+      addChildren(getRef());
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), getRef());
+
+      List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, Matchers.containsInAnyOrder(SagaResponse.NONE_RESPONSE, SagaResponse.NONE_RESPONSE));
+      verify(task, never()).commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE);
+
+      // skip compensation for ignored actor
+      actorRef.tell(compensateMessage, getRef());
+      actorRef.tell(compensateMessage, getRef());
+
+      expectMsg(duration("2 seconds"), compensateMessage);
+      verify(task, never()).compensate(request);
+    }};
+  }
+
+  @Test
+  public void transactIfChosenByAnyParent() throws Exception {
+    when(childrenExtractor.fromJson(Operation.SUCCESSFUL_SAGA_RESPONSE.body())).thenReturn(singleton(requestId));
+
+    new TestKit(actorSystem) {{
+      addChildren(getRef());
+
+      ActorRef parent1 = someActor();
+      context.addParent(requestId, parent1);
+
+      ActorRef parent2 = someActor();
+      context.addParent(requestId, parent2);
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1, parentRequestId2});
+      when(task.commit(eq(request), any(CompositeSagaResponse.class))).thenReturn(response);
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent1);
+      actorRef.tell(new TransactMessage(request2, Operation.SUCCESSFUL_SAGA_RESPONSE), parent2);
+
+      List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, containsInAnyOrder(response, response));
+    }};
+  }
+
+  @Test
+  public void tellTransactionResponseToChildrenOnRecovery() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addChild(requestId, getRef());
+
+      ActorRef parent = someActor();
+      context.addParent(requestId, parent);
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactionRecoveryMessage(response), noSender());
+      actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent);
+
+      List<SagaResponse> responses = receiveN(1, duration("2 seconds")).stream()
+          .map(o -> ((TransactMessage) o).response())
+          .collect(Collectors.toList());
+
+      assertThat(responses, containsInAnyOrder(response));
+
+      verify(task, never()).commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE);
+    }};
+  }
+
+  @Test
+  public void tellCompensationToParentsOnRecovery() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addChild(requestId, someActor());
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new AbortMessage(exception), someActor());
+      actorRef.tell(new CompensationRecoveryMessage(), someActor());
+      actorRef.tell(compensateMessage, someActor());
+
+      List<Object> responses = receiveN(2, duration("2 seconds"));
+      assertThat(responses, contains(instanceOf(AbortMessage.class), instanceOf(CompensateMessage.class)));
+
+      verify(task, never()).compensate(request);
+    }};
+  }
+
+  @Test
+  public void abortOnPersistenceFailure() throws Exception {
+    new TestKit(actorSystem) {{
+      context.addChild(requestId, noSender());
+      context.addParent(requestId, getRef());
+
+      when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+      SagaStartFailedException oops = new SagaStartFailedException("oops", exception);
+      when(task.commit(request, SagaResponse.EMPTY_RESPONSE)).thenThrow(oops);
+
+      ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+      actorRef.tell(new TransactMessage(request, SagaResponse.EMPTY_RESPONSE), getRef());
+
+      expectMsgClass(FailMessage.class);
+    }};
+  }
+
+  private ActorRef someActor() {
+    return actorSystem.actorOf(empty());
+  }
+
+  private void addChildren(ActorRef ref) {
+    context.addChild(requestId, ref);
+    context.addChild(requestId, ref);
+    context.addActor(requestId, ref);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/DirectedAcyclicGraphTraversalTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/DirectedAcyclicGraphTraversalTest.java
new file mode 100644
index 0000000..beb9607
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/DirectedAcyclicGraphTraversalTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.Assert.assertThat;
+
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DirectedAcyclicGraphTraversalTest {
+
+  private final Node<String> root = new Node<>(0, "i don't care");
+  private final Node<String> node1 = new Node<>(1, "i don't care");
+  private final Node<String> node2 = new Node<>(2, "i don't care");
+  private final Node<String> node3 = new Node<>(3, "i don't care");
+  private final Node<String> node4 = new Node<>(4, "i don't care");
+  private final Node<String> node5 = new Node<>(5, "i don't care");
+  private final Node<String> leaf = new Node<>(6, "i don't care");
+
+  private final SingleLeafDirectedAcyclicGraph<String> dag = new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+
+  //        0
+  //       / \
+  //      1   \
+  //     / \   \
+  //    3   4   2
+  //     \ /   /
+  //      5   /
+  //       \ /
+  //        6
+  @Before
+  public void setUp() throws Exception {
+    root.addChildren(asList(node1, node2));
+    node1.addChildren(asList(node3, node4));
+    node3.addChild(node5);
+    node4.addChild(node5);
+    node5.addChild(leaf);
+    node2.addChild(leaf);
+  }
+
+  @Test
+  public void traverseGraphOneLevelPerStepFromRoot() {
+    Traveller<String> traveller = new ByLevelTraveller<>(dag, new FromRootTraversalDirection<>());
+
+    Collection<Node<String>> nodes = traveller.nodes();
+
+    traveller.next();
+    assertThat(nodes, contains(root));
+
+    traveller.next();
+    assertThat(nodes, contains(root, node1, node2));
+
+    traveller.next();
+    assertThat(nodes, contains(root, node1, node2, node3, node4));
+
+    traveller.next();
+    assertThat(nodes, contains(root, node1, node2, node3, node4, node5));
+
+    traveller.next();
+    assertThat(nodes, contains(root, node1, node2, node3, node4, node5, leaf));
+  }
+
+  @Test
+  public void traverseGraphOneLevelPerStepFromLeaf() {
+    Traveller<String> traveller = new ByLevelTraveller<>(dag, new FromLeafTraversalDirection<>());
+
+    Collection<Node<String>> nodes = traveller.nodes();
+
+    traveller.next();
+    assertThat(nodes, contains(leaf));
+
+    traveller.next();
+    assertThat(nodes, contains(leaf, node2, node5));
+
+    traveller.next();
+    assertThat(nodes, contains(leaf, node2, node5, node3, node4));
+
+    traveller.next();
+    assertThat(nodes, contains(leaf, node2, node5, node3, node4, node1));
+
+    traveller.next();
+    assertThat(nodes, contains(leaf, node2, node5, node3, node4, node1, root));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaExecutionComponentTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaExecutionComponentTest.java
new file mode 100644
index 0000000..8515f93
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaExecutionComponentTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import java.util.concurrent.Executors;
+
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaExecutionComponentTestBase;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+
+
+public class GraphBasedSagaExecutionComponentTest extends SagaExecutionComponentTestBase {
+
+  @Override
+  protected SagaFactory sagaFactory(PersistentStore eventStore) {
+    return new GraphBasedSagaFactory(500, eventStore, null, Executors.newFixedThreadPool(5));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBuilderTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBuilderTest.java
new file mode 100644
index 0000000..8703c7a
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBuilderTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.apache.servicecomb.saga.core.Operation.TYPE_REST;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaException;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequestImpl;
+import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.CompensationImpl;
+import org.apache.servicecomb.saga.core.TransactionImpl;
+
+@SuppressWarnings("unchecked")
+public class GraphBuilderTest {
+
+  private final SagaRequest request1 = new SagaRequestImpl(
+      "request-aaa",
+      "aaa",
+      TYPE_REST,
+      new TransactionImpl("/rest/as", "post", emptyMap()),
+      new CompensationImpl("/rest/as","delete", emptyMap())
+  );
+
+  private final SagaRequest request2 = new SagaRequestImpl(
+      "request-bbb",
+      "bbb",
+      TYPE_REST,
+      new TransactionImpl("/rest/bs", "post", emptyMap()),
+      new CompensationImpl("/rest/bs","delete", emptyMap())
+  );
+
+  private final SagaRequest request3 = new SagaRequestImpl(
+      "request-ccc",
+      "ccc",
+      TYPE_REST,
+      new TransactionImpl("/rest/cs", "post", emptyMap()),
+      new CompensationImpl("/rest/cs","delete", emptyMap()),
+      null,
+      new String[]{"request-aaa", "request-bbb"}
+  );
+  private final SagaRequest[] requests = {request1, request2, request3};
+
+  private final SagaRequest duplicateRequest = new SagaRequestImpl(
+      "request-duplicate-id",
+      "xxx",
+      TYPE_REST,
+      new TransactionImpl("/rest/xs", "post", emptyMap()),
+      new CompensationImpl("/rest/xs","delete", emptyMap())
+  );
+  private final SagaRequest[] duplicateRequests = {duplicateRequest, duplicateRequest};
+
+  private final GraphCycleDetector<SagaRequest> detector = Mockito.mock(GraphCycleDetector.class);
+  private final GraphBuilder graphBuilder = new GraphBuilder(detector);
+
+  @Before
+  public void setUp() throws Exception {
+    when(detector.cycleJoints(any())).thenReturn(emptySet());
+  }
+
+  @Test
+  public void buildsGraphOfParallelRequests() {
+    SingleLeafDirectedAcyclicGraph<SagaRequest> tasks = graphBuilder.build(requests);
+
+    Traveller<SagaRequest> traveller = new ByLevelTraveller<>(tasks, new FromRootTraversalDirection<>());
+    Collection<Node<SagaRequest>> nodes = traveller.nodes();
+
+    traveller.next();
+    assertThat(requestsOf(nodes), IsIterableContainingInOrder.contains(NoOpSagaRequest.SAGA_START_REQUEST));
+    nodes.clear();
+
+    traveller.next();
+    assertThat(requestsOf(nodes), contains(request1, request2));
+    nodes.clear();
+
+    traveller.next();
+    assertThat(requestsOf(nodes), contains(request3));
+    nodes.clear();
+
+    traveller.next();
+    assertThat(requestsOf(nodes), IsIterableContainingInOrder.contains(NoOpSagaRequest.SAGA_END_REQUEST));
+  }
+
+  @Test
+  public void blowsUpWhenJsonContainsDuplicateRequestId() {
+    try {
+      graphBuilder.build(duplicateRequests);
+      fail(SagaException.class.getSimpleName() + " is expected, but none thrown");
+    } catch (SagaException e) {
+      assertThat(e.getMessage(),
+          is("Failed to interpret requests with duplicate request id: request-duplicate-id"));
+    }
+  }
+
+  @Test
+  public void blowsUpWhenGraphContainsCycle() {
+    reset(detector);
+    when(detector.cycleJoints(any())).thenReturn(singleton(new Node<>(0L, null)));
+
+    try {
+      graphBuilder.build(requests);
+      expectFailing(SagaException.class);
+    } catch (SagaException e) {
+      assertThat(e.getMessage(), startsWith("Cycle detected in the request graph at nodes "));
+    }
+  }
+
+  private Collection<SagaRequest> requestsOf(Collection<Node<SagaRequest>> nodes) {
+    return nodes.stream()
+        .map(Node::value)
+        .collect(Collectors.toList());
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorTest.java
new file mode 100644
index 0000000..934c54b
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+@SuppressWarnings("unchecked")
+public class GraphCycleDetectorTest {
+
+  private final Node<String> root = node(0);
+  private final Node<String> leaf = node(Long.MAX_VALUE);
+
+  private final Node<String> node1 = node(1);
+  private final Node<String> node2 = node(2);
+  private final Node<String> node3 = node(3);
+
+  private final SingleLeafDirectedAcyclicGraph<String> graph = new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+  private final GraphCycleDetector<String> detector = new GraphCycleDetectorImpl<>();
+
+  @Before
+  public void setUp() throws Exception {
+    root.addChild(node1);
+
+    node2.addChild(leaf);
+    node3.addChild(leaf);
+  }
+
+  @Test
+  public void emptyNodesWhenNoCycleInGraph() {
+    node1.addChild(node2);
+    node1.addChild(node3);
+
+    Set<Node<String>> nodes = detector.cycleJoints(graph);
+
+    assertThat(nodes.isEmpty(), is(true));
+  }
+
+  @Test
+  public void nonEmptyNodesIfGraphContainsCycle() {
+    node1.addChild(node2);
+    node2.addChild(node3);
+    node3.addChild(node1);
+
+    Set<Node<String>> nodes = detector.cycleJoints(graph);
+
+    assertThat(nodes, contains(node1));
+  }
+
+  private Node<String> node(long id) {
+    return new Node<>(id, "value " + id);
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/NodeTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/NodeTest.java
new file mode 100644
index 0000000..2da4ba0
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/NodeTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.core.dag;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class NodeTest {
+
+
+  private final Node<String> parent = new Node<>(0, "i don't care");
+  private final Node<String> node1 = new Node<>(1, "i don't care");
+  private final Node<String> node2 = new Node<>(2, "i don't care");
+  private final Node<String> node3 = new Node<>(3, "i don't care");
+  private final Node<String> node4 = new Node<>(4, "i don't care");
+  private final Node<String> node5 = new Node<>(5, "i don't care");
+  private final Node<String> node6 = new Node<>(6, "i don't care");
+
+  //        0
+  //       / \
+  //      1   \
+  //     / \   \
+  //    3   4   2
+  //     \ /   /
+  //      5   /
+  //       \ /
+  //        6
+  @Before
+  public void setUp() throws Exception {
+    parent.addChildren(asList(node1, node2));
+    node1.addChildren(asList(node3, node4));
+    node3.addChild(node5);
+    node4.addChild(node5);
+    node5.addChild(node6);
+    node2.addChild(node6);
+  }
+
+  @Test
+  public void nodeIsLinkedBidirectionally() {
+    assertThat(parent.children(), containsInAnyOrder(node1, node2));
+    assertThat(node1.parents(), contains(parent));
+    assertThat(node1.children(), containsInAnyOrder(node3, node4));
+
+    assertThat(node2.parents(), contains(parent));
+    assertThat(node2.children(), contains(node6));
+
+    assertThat(node3.parents(), contains(node1));
+    assertThat(node3.children(), contains(node5));
+
+    assertThat(node4.parents(), contains(node1));
+    assertThat(node4.children(), contains(node5));
+
+    assertThat(node5.parents(), containsInAnyOrder(node3, node4));
+    assertThat(node5.children(), contains(node6));
+
+    assertThat(node6.parents(), containsInAnyOrder(node2, node5));
+    assertThat(node6.children().isEmpty(), is(true));
+  }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStoreTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStoreTest.java
new file mode 100644
index 0000000..1216e5f
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStoreTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.saga.infrastructure;
+
+import static org.mockito.Mockito.verify;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.SagaContext;
+import org.apache.servicecomb.saga.core.SagaEvent;
+
+public class ContextAwareEventStoreTest {
+  private final EventStore underlying = Mockito.mock(EventStore.class);
+  private final SagaContext context = Mockito.mock(SagaContext.class);
+  private final SagaEvent sagaEvent = Mockito.mock(SagaEvent.class);
+
+  private final ContextAwareEventStore contextAwareEventStore = new ContextAwareEventStore(underlying, context);
+
+  @Test
+  public void persistWithUnderlyingStore() throws Exception {
+    contextAwareEventStore.offer(sagaEvent);
+
+    verify(sagaEvent).gatherTo(context);
+    verify(underlying).offer(sagaEvent);
+  }
+}
diff --git a/saga-core/src/test/resources/application.conf b/saga-core/src/test/resources/application.conf
new file mode 100644
index 0000000..0629dd6
--- /dev/null
+++ b/saga-core/src/test/resources/application.conf
@@ -0,0 +1,21 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+##      http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+akka {
+  loggers = ["akka.event.slf4j.Slf4jLogger"]
+  loglevel = "DEBUG"
+  logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
+}
diff --git a/saga-core/src/test/resources/log4j2.xml b/saga-core/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..cae04cb
--- /dev/null
+++ b/saga-core/src/test/resources/log4j2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<Configuration status="WARN">
+  <Appenders>
+    <Console name="Console" target="SYSTEM_OUT">
+      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+    </Console>
+  </Appenders>
+  <Loggers>
+    <AsyncRoot level="info">
+      <AppenderRef ref="Console"/>
+    </AsyncRoot>
+  </Loggers>
+</Configuration>
diff --git a/saga-demo/conditional-transaction-demo/README.md b/saga-demo/conditional-transaction-demo/README.md
new file mode 100755
index 0000000..9ae1deb
--- /dev/null
+++ b/saga-demo/conditional-transaction-demo/README.md
@@ -0,0 +1,174 @@
+# Conditional Transaction Demo
+This demo simulates a partial backend of an e-commerce application including four services:
+* payment
+* membership
+* inventory
+* supplier
+
+**Note** Please go through the dependency-free-transaction-demo first, before proceeding with this demo. 
+
+## Background
+Many e-commerce sites have membership concept, which will level up when a customer makes a certain amount of purchase.
+Moreover, as goods are sold and the stock drops to a certain level, the e-commerce company needs to fetch more goods from
+its suppliers. Both scenarios introduce conditional transactions. 
+
+## Workflow
+```
+                  /---> if total purchase >= 1K, level up membership
+start ---> make payment ---> dispatch product from inventory ---> end
+                                              \---> if product stock < 10, replenish product from supplier 
+```
+The conditions are business logic and belong to business service. Therefore, the decision to level up membership is made
+in payment service and the decision to replenish products is made in inventory service. Saga is to be informed of service
+decision by adding an additional element `sagaChildren` in service response JSON to indicate which services are to be invoked
+next as shown below.
+```json
+{
+  "customerId": "mike",
+  "body": "Payment made with id xxx for customer mike",
+  "sagaChildren": ["inventory"] 
+}
+```
+
+If `sagaChildren` is empty or not provided in service response, saga will invoke all of its child services. To invoke none
+of them, explicitly return `"sagaChildren": ["none"]`.
+
+If any of the sub-transaction specified in sagaChildren fails, all completed sub-transactions will be compensated as usual,
+when the recovery policy if backward recovery. 
+
+## Running Demo
+1. run the following command to create docker images in saga project root folder.
+```
+mvn package -DskipTests -Pdocker -Pdemo
+```
+
+2. start application up in saga/saga-demo/conditional-transaction-demo with the following command
+```
+docker-compose up
+```
+
+## User Requests
+The request JSON to ensure the workflow order looks like the following:
+```json
+{
+  "policy": "BackwardRecovery",
+  "requests": [
+    {
+      "id": "payment",
+      "type": "rest",
+      "serviceName": "payment.servicecomb.io:8080",
+      "transaction": {
+        "method": "post",
+        "path": "/payment",
+        "params": {
+          "form": {
+            "customerId": "mike",
+            "purchaseAmount": 400
+          }
+        }
+      },
+      "compensation": {
+        "method": "put",
+        "path": "/payment",
+        "params": {
+          "form": {
+            "customerId": "mike"
+          }
+        }
+      }
+    },
+    {
+      "id": "membership",
+      "type": "rest",
+      "serviceName": "membership.servicecomb.io:8080",
+      "parents": [
+        "payment"
+      ],
+      "transaction": {
+        "method": "post",
+        "path": "/membership",
+        "params": {
+          "form": {
+            "customerId": "mike"
+          }
+        }
+      },
+      "compensation": {
+        "method": "put",
+        "path": "/membership",
+        "params": {
+          "form": {
+            "customerId": "mike"
+          }
+        }
+      }
+    },
+    {
+      "id": "inventory",
+      "type": "rest",
+      "serviceName": "inventory.servicecomb.io:8080",
+      "parents": [
+        "payment"
+      ],
+      "transaction": {
+        "method": "post",
+        "path": "/inventory",
+        "params": {
+          "form": {
+            "customerId": "mike"
+          }
+        }
+      },
+      "compensation": {
+        "method": "put",
+        "path": "/inventory",
+        "params": {
+          "form": {
+            "customerId": "mike"
+          }
+        }
+      }
+    },
+    {
+      "id": "supplier",
+      "type": "rest",
+      "serviceName": "supplier.servicecomb.io:8080",
+      "parents": [
+        "inventory"
+      ],
+      "transaction": {
+        "method": "post",
+        "path": "/supplier",
+        "params": {
+          "form": {
+            "customerId": "servicecomb_mall"
+          }
+        }
+      },
+      "compensation": {
+        "method": "put",
+        "path": "/supplier",
+        "params": {
+          "form": {
+            "customerId": "servicecomb_mall"
+          }
+        }
+      }
+    }
+  ]
+}
+
+```
+
+To send the above JSON request to Saga, use [postman](https://www.getpostman.com/postman) with POST request to url `http://<docker.host.ip>:8083/requests`
+
+Each request to payment service will increase user mike's total purchase by $400. The 3<sup>rd</sup> request will trigger 
+membership level up.
+
+The initial product stock is 11 in inventory and each request to inventory service will deduct product stock by 1. 
+So the 2<sup>nd</sup> request will trigger product replenishment from supplier.
+
+**Note** transactions and compensations implemented by services must be idempotent. In this demo, we did not enforce that
+for simplicity.
+
+To see all events generated by Saga, visit `http://<docker.host.ip>:8083/events` with postman.
diff --git a/saga-demo/conditional-transaction-demo/conditional-transaction-demo-tests/pom.xml b/saga-demo/conditional-transaction-demo/conditional-transaction-demo-tests/pom.xml
new file mode 100644
index 0000000..57a3895
--- /dev/null
+++ b/saga-demo/conditional-transaction-demo/conditional-transaction-demo-tests/pom.xml
@@ -0,0 +1,301 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <artifactId>conditional-transaction-demo</artifactId>
+    <groupId>org.apache.servicecomb.saga.demo</groupId>
+    <version>0.3.0-SNAPSHOT</version>
... 11732 lines suppressed ...