Is Kalix the next step to speed up and simplify software development?

Krzysztof Borowski

What is Kalix?

High-performance microservices and APIs with no operations required

The promise

Diagram showing the parts of a mature flower. In this example the perianth is separated into a calyx (sepals) and corolla (petals)

brew install kalix

kalix auth login

kalix projects new <projectname> "project description" --region=<region>

kalix config set project <projectname>

kalix docker add-credentials --docker-server <my-server> \ 
  --docker-username <my-username> \ 
  --docker-email <my-email> \ 
  --docker-password <my-password> 

Setup the project

PhotoTrip App

  • Create personal map
  • Add new visited places
  • Add photo links to the place
  • Display system stats

Architecture oversimplified

#!/bin/sh

sbt Docker/publish -Ddocker.username=liosedhel &&
docker tag liosedhel/phototrip:$1 &&
docker push liosedhel/phototrip:$1 &&

kalix service deploy phototrip liosedhel/phototrip:$1
kalix services expose my-service

Deploy!

Management

kalix projects list
kalix logs --raw phototrip
kalix svc list
kalix svc components list phototrip

Creating a PhotoTrip App

Aggregates = Entities

Entity

  • Can be referenced from outside (ID).
  • A cluster of domain objects that can be treated as a single unit.
  • Aggregates domain objects and theirs behaviour.
  • Every change of the state within aggregate is transactional.

Kalix Entities

Value Entity

Value Entity

Value Entity - Scaling

 Value Entity - WorldMap

message CreateWorldMap {
  string map_id = 1 [(kalix.field).entity_key = true];
  string creator_id = 2;
  string description = 3;
}

service WorldMapService {
  option (kalix.codegen) = {
    value_entity: {
      name: "com.virtuslab.phototrip.worldmap.domain.WorldMapValueEntity"
      entity_type: "worldmap"
      state: "com.virtuslab.phototrip.worldmap.domain.WorldMapState"
    }
  };
  option (kalix.service).acl.allow = { principal: ALL };

  rpc Create (CreateWorldMap) returns (google.protobuf.Empty);
  rpc Get (GetWorldMap) returns (CurrentWorldMap) {
    option (google.api.http) = {
      get: "/worldmaps/{worldmap_id}"
    };
  };
}

Value Entity - WorldMap

class WorldMapValueEntity(context: ValueEntityContext) 
  extends AbstractWorldMapValueEntity {
    
  override def emptyState: WorldMap = WorldMap()

  override def create(
    currentState: WorldMap, 
    createWorldMap: CreateWorldMap
  ): ValueEntity.Effect[Empty] = {
    if(!isValid(createWorldMap)) {
      effects.error(s"Invalid $createWorldMap command")
    } else {
      effects
      	.updateState(toWorldMap(createWorldMap))
        .thenReply(Empty.defaultInstance)
    }
  }

  override def get(
    currentState: WorldMap, 
    getWorldMap: GetWorldMap
  ): ValueEntity.Effect[CurrentWorldMap] = {
    if (currentState == emptyState) {
      effects.error(s"Map ${getWorldMap.worldmapId} does not exist")
    } else {
      effects.reply(toCurrentWorldMap(currentState))
    }
  }
  ...
}

Event Sourced Entity

Event Sourced Entity

Event Sourced Entity - Place

service PlaceService {
  option (kalix.codegen) = {
    event_sourced_entity: {
      name: "com.virtuslab.phototrip.place.domain.PlaceEventSourcedEntity"
      entity_type: "place"
      state: "com.virtuslab.phototrip.place.domain.Place"
      events: [
        "com.virtuslab.phototrip.place.domain.PlaceCreated",
        "com.virtuslab.phototrip.place.domain.PhotoLinkAdded"
      ]
    }
  };
  option (kalix.service).acl.allow = { principal: ALL };

  rpc CreatePlace (CreateNewPlace) returns (google.protobuf.Empty);
  rpc AddPhotoLink (AddPhotoLinkUrl) returns (google.protobuf.Empty);
  rpc Get (GetPlace) returns (CurrentPlace) {
    option (google.api.http) = {
      get: "/places/{place_id}"
    };
  };
}

Event Sourced Entity - Place

class PlaceEventSourcedEntity(context: EventSourcedEntityContext)
  extends AbstractPlaceEventSourcedEntity {

  override def emptyState: Place = Place()

  override def createPlace(
    currentState: Place, 
    createNewPlace: CreateNewPlace
  ): EventSourcedEntity.Effect[Empty] =
    effects.emitEvent(
      PlaceCreated(
        createNewPlace.placeId, 
        createNewPlace.mapId, 
        createNewPlace.description, 
        createNewPlace.coordinates
      )
    ).thenReply(_ => Empty.defaultInstance)

  override def placeCreated(
    currentState: Place, 
    placeCreated: PlaceCreated
  ): Place =
    Place(
      placeCreated.placeId, 
      placeCreated.mapId, 
      placeCreated.description, 
      placeCreated.coordinates, 
      Nil
    )
  

Replicated Entity

Replicated Entity - User

Replicated Entity - User

Replicated Entity - User

service UserService {
  option (kalix.codegen) = {
    replicated_entity: {
      name: "com.virtuslab.phototrip.user.domain.UserReplicatedEntity"
      entity_type: "user"
      replicated_register: {
        value: "com.virtuslab.phototrip.user.domain.User"
      }
    }
  };
  option (kalix.service).acl.allow = { principal: ALL };
  rpc Create (CreateNewUser) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      post: "/users"
    };
  };
  rpc Get (GetUser) returns (com.virtuslab.phototrip.user.domain.User) {
    option (google.api.http) = {
      get: "/users/{user_id}"
    };
  };
}

Replicated Entity - User

class UserReplicatedEntity(context: ReplicatedEntityContext)
  extends AbstractUserReplicatedEntity {

  override def emptyValue: User = User()

  override def create(
    currentData: ReplicatedRegister[User],
    createNewUser: CreateNewUser
  ): ReplicatedEntity.Effect[Empty] = {
    effects
      .update(
        currentData.set(
          User(createNewUser.userId, createNewUser.email, createNewUser.nick)
        )
      ).thenReply(Empty.defaultInstance)
  }

  def get(
    currentData: ReplicatedRegister[User],
    getUser: api.GetUser
  ): ReplicatedEntity.Effect[User] = {
    currentData.get match {
      case Some(user) => effects.reply(user)
      case None =>
        effects.error("User does not exist", io.grpc.Status.Code.NOT_FOUND)
    }
  }
}

Views

View - World map by user

View - World map by user

service WorldMapByUserId {
  option (kalix.codegen) = {
    view: {}
  };

  rpc UpdateWorldMap(domain.WorldMapState) returns (WorldMapView) {
    option (kalix.method).eventing.in = {
      value_entity: "worldmap"
    };
    option (kalix.method).view.update = {
      table: "worldmaps"
      transform_updates: true
    };
  }

  rpc GetWorldMaps(ByUserIdRequest) returns (stream WorldMapView) {
    option (kalix.method).view.query = {
      query: "SELECT * FROM worldmaps WHERE creator_id = :user_id"
    };
    option (google.api.http) = {
      get: "/worldmap/user/{user_id}"
    };
  }

}

View - World map by user

class WorldMapByUserIdView(context: ViewContext) 
  extends AbstractWorldMapByUserIdView {

  override def emptyState: WorldMapView = WorldMapView.defaultInstance

  override def updateWorldMap(
    state: WorldMapView,
    worldMapState: WorldMapState
  ): UpdateEffect[WorldMapView] = {
    effects.updateState(
      WorldMapView(
        worldMapState.mapId,
        worldMapState.creatorId,
        worldMapState.description
      )
    )
  }

}

PubSub

aka glue it together

PubSub Configuration

$ kalix projects config set broker \
	--broker-service kafka \
    --broker-config-file <kafka-broker-config-file>
    
    
ccloud kafka topic create TOPIC_ID

Publishing to PubSub

service WorldMapAndPlacesEventsToTopic {
  option (kalix.codegen) = {
    action: {}
  };

  rpc PlaceCreation (com.virtuslab.phototrip.place.domain.PlaceCreated) 
  	returns (PlaceCreatedMessage) {
    option (kalix.method).eventing.in = {
      event_sourced_entity: "place"
      ignore_unknown: true
    };
    option (kalix.method).eventing.out = {
      topic: "analytics-events"
    };
  }

  rpc WorldMapUpdate (com.virtuslab.phototrip.worldmap.domain.WorldMapState) 
  	returns (WorldMapUpdatedMessage) {
    option (kalix.method).eventing.in = {
      value_entity: "worldmap"
    };
    option (kalix.method).eventing.out = {
      topic: "analytics-events"
    };
  }

}

Publishing to PubSub

class WorldMapAndPlacesEventsToTopicAction(creationContext: ActionCreationContext)
  extends AbstractWorldMapAndPlacesEventsToTopicAction {

  override def placeCreation(
    placeCreated: PlaceCreated
  ): Action.Effect[PlaceCreatedMessage] = {
    effects.reply(
      PlaceCreatedMessage(
        placeCreated.placeId,
        placeCreated.mapId,
        placeCreated.description,
        placeCreated.coordinates.map(c => Coordinates(c.latitude, c.latitude))
      )
    )
  }
  
  override def worldMapUpdate(
   worldMapState: WorldMapState
  ): Action.Effect[WorldMapUpdatedMessage] = {
    effects.reply(
      WorldMapUpdatedMessage(
        worldMapState.mapId,
        worldMapState.creatorId,
        worldMapState.description
      )
    )
  }
}

Reading from PubSub

service ReadFromAnalyticsTopic {
  option (kalix.codegen) = {
    action: {}
  };

  rpc PlaceCreation (com.virtuslab.phototrip.worldmap.actions.PlaceCreatedMessage) 
      returns (google.protobuf.Empty) {
    option (kalix.method).eventing.in = {
      topic: "analytics-events"
    };
  }

  rpc WorldMapUpdate (com.virtuslab.phototrip.worldmap.actions.WorldMapUpdatedMessage) 
      returns (google.protobuf.Empty) {
    option (kalix.method).eventing.in = {
      topic: "analytics-events"
    };
  }

}

Reading from PubSub

class ReadFromAnalyticsTopicAction(creationContext: ActionCreationContext) 
  extends AbstractReadFromAnalyticsTopicAction {

  private val log = LoggerFactory.getLogger(classOf[ReadFromAnalyticsTopicAction])

  override def placeCreation(
    placeCreatedMessage: PlaceCreatedMessage
  ): Action.Effect[Empty] = {
    log.info(s"PubSub:Read $placeCreatedMessage")
    effects.forward(
      components.statsValueEntity.placeCreation(
        NewPlace(StatsValueEntity.key, placeCreatedMessage.placeId)
      )
    )
  }
  override def worldMapUpdate(
   worldMapUpdatedMessage: WorldMapUpdatedMessage
  ): Action.Effect[Empty] = {
    log.info(s"PubSub:Read $worldMapUpdatedMessage")
    effects.forward(
      components.statsValueEntity.mapUpdate(
        NewMap(StatsValueEntity.key, worldMapUpdatedMessage.mapId)
      )
    )
  }
}

Managing Components

kalix svc c list-entity-ids phototrip \
    com.virtuslab.phototrip.place.api.PlaceService

kalix svc c get-state phototrip \
    com.virtuslab.phototrip.place.api.PlaceService Place1 --output json

kalix svc c list-events phototrip \
    com.virtuslab.phototrip.place.api.PlaceService Place1 --output json

kalix svc views list phototrip

Developer Experience

Developer Experience - Writing the code

Protobuf first approach

Pros

  • All built components have clearly stated APIs.
  • Code stubs and test stubs are auto-generated based on the proto definition.
  • gRPC and REST endpoints “for free”.

Cons

  • You need to learn proto and gRPC DSL first.
  • Poor experience when refactoring.
  • Lost Scala-ish typesafety.
  • Runtime errors.
  • Protobuf serde only (or some workaround with proto and Any type).

Abstract high-level components

Pros

  • You can jump straight to the business logic (no infra layer).
  • Scalable, following the Single-Writer principle and optionally event sourced, entities available out of the box.
  • Glue it with configuration.

Cons

  • Quite specific components, different to what most programmers are used to.
  • Some useful features are under development (like view joins, sagas).
  • Limited configuration options or performance tweaking.

Writing tests

Pros

  • Easy to start and extend pregenerated unit tests.
  • Easy to start and test manually particular Kalix service.

Cons

  • Integration tests only on dev environment (costly in the long run) (* soon it should be addressed by LB).
  • Local testing is done with in-memory database only.

Developer Experience - managing the Kalix services

Running services locally

Pros

  • Relatively straightforward to run one service locally.
  • It can help to catch most of the potential runtime errors.

Cons

  • Stateless only - can’t help with testing backward compatibility.
  • Tricky to run multiple services or configure and test the service-to-service connection.
docker-compose up -d

sbt run

Managing Kalix projects and services via CLI

Pros

  • One tool to invoke any Kalix related operations.
  • Allows to view your data.
  • Under active development.

Cons

  • Yet another CLI syntax to learn.
  • Under active development.
  • Views have to be browsed. through gRPC calls directly.
kalix svc start phototrip
kalix svc expose phototrip
kalix svc components list phototrip
kalix svc views list phototrip
kalix svc delete phototrip

Prod deployment

Pros

  • Easy to deploy a new version.
  • Zero downtime deployment, scaling, whole provisioning is out of the box.
  • Extremaly low time to market.

Cons

  • No schema backward compatibility check mechanism.
  • Limited visibility into the platform - in case of cryptic error the ticket is required.
kalix service deploy \
	phototrip liosedhel/phototrip:55

Managing persistent data

Pros

  • You don’t have to manage the database.
  • You don’t have to implement infrastructure level (both database and messaging related).
  • Infra solution is well adjusted to Kalix programming model.

Cons

  • You don’t have direct access to your data.
  • You can’t do your own backups (feature in progress).
  • You can break your data consistency (no schema check).
  • Data migration only through protobuf schema adjustment way.

Performance and scalability

Pros

  • Polished and proven akka technology wrapped in convenient high-level API deployed with Kubernetes capabilities.
  • You get automatic horizontal scalability without any manual setup or integrations.
  • Each entity state is loaded to the memory - effectively serve as cache layer.

Cons

  • Each recently used entity state is loaded to the memory - services can easily become memory intensive.
  • To achieve optimal performance, you need to know the underlying computation technology (e.g. to not block inside entity logic as it might block underlying dispatcher).
  • There is no way to tweak autoscaling capabilities.

Integrating external services

Pros

  • Integration through high level contract first API.
  • External services can be accessed and integrated through Kalix actions.
  • Kalix services by default can call any publically available service

Cons

  • Only possible through additional abstraction level (wrapping external calls with effects).
  • Has to be integrated over a public internet connection.

Other considerations

Pricing

You can get a custom deal, see https://discuss.kalix.io/t/minimum-per-service-cost-on-pay-as-you-go/163.

Additionally, the feature to put some upper bound or tweak autoscaling service capability is on the way - this should reduce unexpected costs in case of unpredicted service traffic.

Subjective: most important Kalix missing capabilities

  • IAM integration - no way to integrate directly with existing organisation accounts.

  • No direct integration network (e.g. VPC Network Peering) between Kalix platform and client infrastructure (available with custom deal).

  • Limited views capabilities (features in progress).

  • Scalability only within a particular region (feature in progress).

  • No external data backup (feature in progress)

Is Kalix a next step in software development?

Useful materials

PhotoTrip with Kalix

By liosedhel

PhotoTrip with Kalix

  • 167