Kalix simplifies application development by allowing developers to focus on their domain models and APIs without worrying about how the data is stored.
Your responsibility as a developer is to implement the necessary Components that encapsulate the state and business logic of the system you are building.
There are four main types of Components:
Entities are domain objects that handle commands and events, manage concurrency and persist state. They are uniquely identified by a key. The developer decides which type of Entity to use, depending on the use case. Learn more about different entity use cases here.
Views enable the developer to retrieve and transform data from multiple entities using a SQL-like query language.
Actions are stateless functions used for logic that doesn’t require data persistence. Actions can be triggered by changes in other Components, a gRPC service call or an HTTP service call.
Workflows enable the developer to implement long-running, multi-step business processes while focusing exclusively on domain and business logic. Technical concerns such as delivery guarantees, scaling, error handling and recovery are managed by Kalix.
A Service contains the code you write using various abstractions called Components. Services are invoked through requests via HTTP, gRPC, messages to topics, or from other Kalix Services. Clients, such as user interfaces or external services, can be built using any framework.
In traditional architectures, developers need to handle database connections, maintenance, and scalability. However, in Kalix, each Service is supported by the Runtime that handles all persistence concerns automatically. This removes the need for developers to manage databases, and the runtime handles data access, transactions, scaling, caching and failover.
When a client makes a request to a Kalix Service, the request is received by the Service’s API endpoints, managed by the Kalix Runtime, and then routed to the appropriate Component for processing.
By leveraging Kalix’s capabilities, you can focus on implementing your Service’s functionality while Kalix takes care of data persistence and retrieval, resulting in a more efficient and manageable development process.
Kalix favors an architecture style similar to the Onion Architecture style. It is called "Onion Architecture" because the application is built in layers, similar to an onion. The layers in Onion Architecture are represented concentrically, with the core or the most important part of the application at the center. The other layers surround this core, each having a specific role.
The outermost layer is the infrastructure layer, which contains the implementation of infrastructure concerns such as persistence, messaging, communication and security. This layer is completely managed by Kalix. As a developer, you don’t need to worry about it. Kalix takes care of all the infrastructure for you.
The middle layer is the application layer. It’s dependent on the domain model (innermost layer) but independent of the infrastructure (outermost layer). In Kalix, this layer is where your Actions, Entities, Workflows and Views will live. In this sense, a Kalix Component acts as the glue between your domain model and the Kalix Runtime.
The Kalix Runtime interacts with the application layer and interprets the operations to be performed for you. Each component defines a set of operations through its application programming interface (API). These operations are specific to the semantics of each component. For example, an Event Sourced Entity allows you to define command handlers that declaratively describe which events to persist, how to build a response using the updated model state, etc. Whereas a Kalix Action allows you to subscribe to events, publish events to topics or simply reply to an incoming request. These operations are then interpreted by the Kalix Runtime and executed on your behalf.
Finally, at the heart of the application is the Domain Model - the innermost layer that encapsulates the business logic. When architecting a Kalix Service, this core layer becomes the foundation upon which your service takes shape and form.
Kalix significantly simplifies distributed application development. By isolating the business logic into Components and taking care of the infrastructure concerns around them, Kalix makes it possible to abstract away the complexities of distributed systems.
Each Kalix component is a self-contained and isolated unit of business logic. This is an important property in a distributed system platform as it allows the platform to scale out the application according to its need. For example, Kalix can freely make decisions about where to instantiate the components across the different running instances of a service or even move instances from one server to another in order to better distribute the load.
As a consequence, a given component instance doesn’t have a fixed location in the cluster. This property is called Location Transparency. Its location is not observable from the outside and can change at any time.
When implementing a Kalix service, you must keep in mind that you are building a distributed system where location transparency is a fundamental property. Therefore, whenever you interact with a component, this is done through a network call to the Kalix Runtime. The Kalix Runtime knows where the component instance is located and forwards the request to it.
Stateful components such as Entities and Workflows represent islands of state consistency. By design, they demarcate a transactional boundary. They can only be mutated individually, and it’s simply not possible to mutate two stateful components at the same time since they may be instantiated in two different physical machines.
This feature of Kalix is what allows it to scale horizontally. It also means that you have to be aware of the implications of building a distributed system. Kalix solves many of the hard problems of distributed systems for you, but you need to be aware that each component is a self-contained unit of business logic instantiated somewhere in the cluster. Therefore, you need to get familiar and embrace concepts such as eventual consistency, at-least-once delivery and idempotency in your application design.
When building a Kalix service, there are two types of consistency to consider: strong consistency and eventual consistency.
Entities and workflows are strongly consistent. When you send a command to an Entity or Workflow instance, it’s guaranteed that the command will operate on the latest known state. Concurrency is also guaranteed. If two commands are sent to the same Entity or Workflow instance, they will be processed sequentially.
On the other hand, components that react to changes from entities or to messages published to a Topic are eventually consistent. For instance, if you have a View that subscribes to events from a particular Event Sourced Entity, the events will be delivered to the View with some delay compared to the time they were effectively persisted. This delay is often very short (between a few milliseconds and a few seconds, depending on system load), but it can be noticeable if you access the View immediately after modifying the Entity.
The same applies for Actions subscribing to events emitted by an Event Sourced Entity. The events will be delivered to the Action with some delay compared to the time they were effectively persisted.
This brings us to the next concept: at-least-once delivery.
When an Action or a View subscribes to some data, be it events from an Event Sourced Entity, state changes from a Value Entity, or simply messages from a Topic, the delivery guarantees will always be at-least-once.
The mechanics behind this are as follows:
Kalix Runtime detects that it has some new data to deliver, e.g.: an event from an Entity.
Kalix Runtime makes a call to the method you defined to handle this type of event.
Your method performs the operation it was designed for and returns successfully.
Kalix stores the information that this event was delivered to your method.
It turns out that such an exchange can fail at several points and trigger redelivery.
For instance, after calling the method, its response may never reach the Kalix Runtime due to some network failure. Or, you receive the event in an Action method, make a call to some external service, the service receives your call but fails to acknowledge. The Action method will fail with a timeout, causing the Kalix Runtime to redeliver the event to the method, as it’s impossible for Kalix to know if the operation was successful or not. On redelivery, the Action method will again reach out to the external service.
This is essentially why we call it an at-least-once delivery guarantee. The data is guaranteed to be delivered, but can be delivered more than once if there are failures at any point in the exchange.
Idempotency states that a given operation can be performed many times without changing the end result beyond the first time it is performed. In other words, no matter how many times you repeat the operation, the result remains the same after the first successful execution of the operation.
Idempotency is crucial in building fault-tolerant systems, especially over a network, because if a request fails, it can be safely retried without worrying about unintended side effects from multiple executions.
A rule of thumb for building distributed systems is that any time you are dealing with at-least-once delivery guarantees, you need to match it with idempotency. Because choosing the idempotency strategy is very often driven by the actual business logic, it’s on a developer to decide how to handle redeliveries.