Running Side Effects in Java

Emitting effects on another component

An Entity or an Action may also emit one or more side effects. A side effect is something whose result has no impact on the result of the current command—​if it fails, the current command still succeeds. The result of the side effect is therefore ignored. When used from inside an Entity, side effects are only performed after the successful completion of any state actions requested by the command handler.

There is no guarantee that a side effect will be executed successfully. If a failure occurs after the command is fully handled, effects might not be executed. Side effects are not retried in case of failures.

Side effects may be declared as synchronous or asynchronous. Asynchronous commands run in a "fire and forget" fashion. The code flow of the caller (the command handler of the entity which emitted the asynchronous command) continues while the command is being asynchronously processed. Meanwhile, synchronous commands run sequentially, that is, the commands are processed in order, one at a time. The final result of the command handler, either a reply or a forward, is not sent until all synchronous commands are completed.

Use case: mobile notification

You might want to run side effects to notify interested parties of a change in state. For example, after a withdrawal is made from a bank account, an account entity could send a notification to the account owner’s mobile phone.

Emitting a side effect

To illustrate how you can emit a side effect, we can build on top of the Action as a Controller example. In that previous example, we build a controller around the Value Entity Counter and forwarded the incoming request after modifying it.

This time, instead of using a forward, we will call the entity using a side effect.

Java
src/main/proto/com/example/actions/double-counter.proto
syntax = "proto3";
package com.example.actions;

import "akkaserverless/annotations.proto";
import "com/example/counter_api.proto"; (1)
import "google/protobuf/empty.proto";

option java_outer_classname = "DoubleCounterApi";

service DoubleCounter {
  option (akkaserverless.service) = {
    type : SERVICE_TYPE_ACTION  (2)
  };

  rpc Increase (com.example.IncreaseValue) returns (google.protobuf.Empty); (3)

  rpc IncreaseWithSideEffect (com.example.IncreaseValue) returns (google.protobuf.Empty); (4)

  rpc forwardWithGrpcApi (com.example.IncreaseValue) returns (google.protobuf.Empty);
  rpc sequentialComposition (com.example.IncreaseValue) returns (com.example.CurrentCounter);
  rpc sumOfMy3FavouriteCounterValues (google.protobuf.Empty) returns (com.example.CurrentCounter);

}
1 Import the Counter API definition
2 The protobuf option (akkaserverless.service) is specific to code-generation as provided by the Akka Serverless Maven plugin. This annotation indicates to the code-generation that an Action must be generated.
3 The Action mimics the Counter API with a forward
4 The Action mimics the Counter API with a side effect
Scala
src/main/proto/com/example/actions/double-counter.proto
syntax = "proto3";
package com.example.actions;

import "akkaserverless/annotations.proto";
import "com/example/counter_api.proto"; (1)
import "google/protobuf/empty.proto";

service DoubleCounter {
  option (akkaserverless.service) = {
    type : SERVICE_TYPE_ACTION  (2)
  };

  rpc Increase (com.example.IncreaseValue) returns (google.protobuf.Empty); (3)

  rpc IncreaseWithSideEffect (com.example.IncreaseValue) returns (google.protobuf.Empty); (4)

}
1 Import the Counter API definition
2 The protobuf option (akkaserverless.service) is specific to code-generation as provided by the Akka Serverless Maven plugin. This annotation indicates to the code-generation that an Action must be generated.
3 The Action mimics the Counter API with a forward
4 The Action mimics the Counter API with a side effect

Implementing the Action

The class DoubleCounterAction gets generated for us based on the same proto file defined in Action as a Controller.

src/main/java/com/example/actions/DoubleCounterAction.java
/**
 * An action.
 */
public class DoubleCounterAction extends AbstractDoubleCounterAction {

  public DoubleCounterAction(ActionCreationContext creationContext) {
  }


  // Handler for "Increase" not shown in this snippet

  /**
   * Handler for "IncreaseWithSideEffect".
   */
  @Override
  public Effect<Empty> increaseWithSideEffect(CounterApi.IncreaseValue increaseValue) {
    int doubled = increaseValue.getValue() * 2;
    CounterApi.IncreaseValue increaseValueDoubled =
        increaseValue.toBuilder().setValue(doubled).build(); (1)

    return effects()
            .reply(Empty.getDefaultInstance()) (2)
            .addSideEffect( (3)
                SideEffect.of(components().counter().increase(increaseValueDoubled)));
  }
}
1 On incoming requests, we double the value of IncreaseValue
2 We build a reply using Empty.getDefaultInstance().
3 and we attach a side effect to it. The side effect is the call to the Counter entity.

Please note that, the result of a side effect is ignored by the current command meaning that even if the call to the Counter entity fails, the Action reply will succeed.

Unit testing the side effects

The side effects of an Action can be tested in isolation. To test the side effects of DoubleCounterAction, shown on the previous snippet, we can leverage ActionResult new tab. This class has the method getSideEffects() that returns the list of side effects added to the Action.

Java
src/test/java/com/example/actions/DoubleCounterActionTest.java
  @Test
  public void increaseWithSideEffectTest() {
    DoubleCounterActionTestKit testKit = DoubleCounterActionTestKit.of(DoubleCounterAction::new);
    int increase = 3;
    CounterApi.IncreaseValue increaseValueCommand = CounterApi.IncreaseValue.newBuilder()
        .setValue(increase)
        .build();
    ActionResult<Empty> result1 = testKit.increaseWithSideEffect(increaseValueCommand);(1)
    DeferredCallDetails<?, ?> sideEffect = result1.getSideEffects().get(0);(2)
    assertEquals("com.example.CounterService", sideEffect.getServiceName());(3)
    assertEquals("Increase", sideEffect.getMethodName());(4)
    CounterApi.IncreaseValue doubledIncreased =  CounterApi.IncreaseValue.newBuilder()
        .setValue(increase * 2)
        .build();
    assertEquals(doubledIncreased, sideEffect.getMessage());(5)
  }
}
1 executing the DoubleCounterAction.increase RPC call through the test kit
2 retrieving the first side effect. There is only one in DoubleConterAction.increase. It’s worth noting the side effects are DeferredCall objects that represent Akka Serverless RPC services. DeferredCallDetails is the representation of a DeferredCall on the Akka Serverless test kit framework.
3 retrieving and asserting the name of the service
4 retrieving and asserting the RPC’s name of the service
5 retrieving and asserting the RPC’s input
Scala
src/test/scala/com/example/actions/DoubleCounterActionSpec.java
  "DoubleCounterAction" must {
    "handle command IncreaseWithSideEffect" in {
      val testKit = DoubleCounterActionTestKit(new DoubleCounterAction(_))

      val result: ActionResult[Empty] = testKit.increaseWithSideEffect(IncreaseValue(value = 1))(1)
      result.reply shouldBe Empty.defaultInstance

      val sideEffect = result.sideEffects.head (2)
      sideEffect.serviceName shouldBe "com.example.CounterService" (3)
      sideEffect.methodName shouldBe "Increase" (4)
      sideEffect.message shouldBe IncreaseValue(value = 2) (5)
    }
  }
}
1 executing the DoubleCounterAction.increaseWithSideEffect RPC call through the test kit
2 retrieving the first side effect. There is only one in DoubleConterAction.increaseWithSideEffect implementation. It’s worth noting the side effects are DeferredCall objects that represent Akka Serverless RPC services. DeferredCallDetails is the representation of a DeferredCall on the Akka Serverless test kit framework.
3 retrieving and asserting the name of the service
4 retrieving and asserting the RPC’s name of the service
5 retrieving and asserting the RPC’s input