Backend Actor logic

In this example, the backend only uses one basic actor. In a real system, we would have many actors interacting with each other and perhaps, multiple data stores and microservices.

An interesting side-note to add here is about when using actors in applications like this adds value over just providing functions that would return CompletionStages. In fact, if your logic is stateless and very simple request/reply style, you may not need to back it with an Actor. Actors do shine when you need to keep some form of state and allow various requests to access something in (or through) an Actor. The other stellar feature of actors, that futures would not handle, is scaling-out onto a cluster very easily, by using Cluster Sharding or other location-transparent techniques.

However, the focus of this tutorial is on how to interact with an Actor backend from within Akka HTTP – not on the actor itself, so we’ll keep it very simple.

The sample code in the UserRegistry is very simple. It keeps registered users in a Set. Once it receives messages it matches them to the defined cases to determine which action to take:

public class UserRegistry extends AbstractBehavior<UserRegistry.Command>  {

  // actor protocol
  interface Command {}

  public final static class GetUsers implements Command {
    public final ActorRef<Users> replyTo;
    public GetUsers(ActorRef<Users> replyTo) {
      this.replyTo = replyTo;
    }
  }

  public final static class CreateUser implements Command {
    public final User user;
    public final ActorRef<ActionPerformed> replyTo;
    public CreateUser(User user, ActorRef<ActionPerformed> replyTo) {
      this.user = user;
      this.replyTo = replyTo;
    }
  }

  public final static class GetUserResponse {
    public final Optional<User> maybeUser;
    public GetUserResponse(Optional<User> maybeUser) {
      this.maybeUser = maybeUser;
    }
  }

  public final static class GetUser implements Command {
    public final String name;
    public final ActorRef<GetUserResponse> replyTo;
    public GetUser(String name, ActorRef<GetUserResponse> replyTo) {
      this.name = name;
      this.replyTo = replyTo;
    }
  }


  public final static class DeleteUser implements Command {
    public final String name;
    public final ActorRef<ActionPerformed> replyTo;
    public DeleteUser(String name, ActorRef<ActionPerformed> replyTo) {
      this.name = name;
      this.replyTo = replyTo;
    }
  }


  public final static class ActionPerformed implements Command {
    public final String description;
    public ActionPerformed(String description) {
      this.description = description;
    }
  }

  public final static class User {
    public final String name;
    public final int age;
    public final String countryOfResidence;
    @JsonCreator
    public User(@JsonProperty("name") String name, @JsonProperty("age") int age, @JsonProperty("countryOfRecidence") String countryOfResidence) {
      this.name = name;
      this.age = age;
      this.countryOfResidence = countryOfResidence;
    }
  }

  public final static class Users{
    public final List<User> users;
    public Users(List<User> users) {
      this.users = users;
    }
  }

  private final List<User> users = new ArrayList<>();

  private UserRegistry(ActorContext<Command> context) {
    super(context);
  }

  public static Behavior<Command> create() {
    return Behaviors.setup(UserRegistry::new);
  }

  @Override
  public Receive<Command> createReceive() {
    return newReceiveBuilder()
        .onMessage(GetUsers.class, this::onGetUsers)
        .onMessage(CreateUser.class, this::onCreateUser)
        .onMessage(GetUser.class, this::onGetUser)
        .onMessage(DeleteUser.class, this::onDeleteUser)
        .build();
  }

  private Behavior<Command> onGetUsers(GetUsers command) {
    // We must be careful not to send out users since it is mutable
    // so for this response we need to make a defensive copy
    command.replyTo.tell(new Users(Collections.unmodifiableList(new ArrayList<>(users))));
    return this;
  }

  private Behavior<Command> onCreateUser(CreateUser command) {
    users.add(command.user);
    command.replyTo.tell(new ActionPerformed(String.format("User %s created.", command.user.name)));
    return this;
  }

  private Behavior<Command> onGetUser(GetUser command) {
    Optional<User> maybeUser = users.stream()
        .filter(user -> user.name.equals(command.name))
        .findFirst();
    command.replyTo.tell(new GetUserResponse(maybeUser));
    return this;
  }

  private Behavior<Command> onDeleteUser(DeleteUser command) {
    users.removeIf(user -> user.name.equals(command.name));
    command.replyTo.tell(new ActionPerformed(String.format("User %s deleted.", command.name)));
    return this;
  }

}

If you feel you need to brush up on your Akka Actor knowledge, the Getting Started Guide reviews actor concepts in the context of a simple Internet of Things (IoT) example.