Zitadel's Software Architecture | ZITADEL Docs
Zitadel's Software Architecture
Zitadel is built with two essential patterns: Event Sourcing (ES) and Command and Query Responsibility Segregation (CQRS). Due to the nature of Event Sourcing, Zitadel provides the unique capability to generate a strong audit trail of ALL the things that happen to its resources, without compromising on storage cost or audit trail length.
The combination of ES and CQRS makes Zitadel eventually consistent, which is a great benefit in many ways. It allows us to build a Source of Records (SOR), which is the one single point of truth for all computed states. The SOR needs to be transaction safe to make sure all operations are in order. You can read more about this in our ES documentation.
Each Zitadel binary contains all components necessary to serve traffic, including serving the API, rendering GUIs, and background processing of events and tasks. This All in One (AiO) approach makes operating Zitadel simple.
The Architecture
Zitadel's software architecture is built around multiple components at different levels. This chapter should give you an idea of the components as well as the different layers.
Service Layer
The service layer includes all components which are potentially exposed to consumers of Zitadel.
HTTP Server
The HTTP server is responsible for the following functions:
- Serving the management GUI called Zitadel Management Console
- Serving static assets
- Rendering server-side HTML (login, password-reset, verification, ...)
API Server
The API layer consists of multiple APIs provided by Zitadel. Each serves a dedicated purpose. All APIs of Zitadel are always available as gRPC, gRPC-web, and REST service. The only exception is the OpenID Connect & OAuth and Asset API due to their unique nature.
- OpenID Connect & OAuth - allows requesting authentication and authorization of Zitadel
- SAML - allows requesting authentication and authorization of Zitadel through the SAML standard
- Authentication API - allows a user to perform operations in their own context
- Management API - allows an admin or machine to manage Zitadel resources at the organization level
- Administration API - allows an admin or machine to manage Zitadel resources at an instance level
- System API - allows creating and changing new Zitadel instances
- Asset API - is used to upload and download static assets
Core Layer
Commands
The Command Side has some unique requirements:
- Transaction safety is a MUST
- Availability MUST be high
When we classify this with the CAP theorem, we would choose Consistent and Available but leave Partition Tolerance aside.
Command Handler
The command handler receives all operations that alter a resource managed by Zitadel. For example, if a user changes their name, the API Layer will pass the instruction received through the API call to the command handler for further processing. The command handler is then responsible for creating the necessary commands and handing them down to the command validation.
Command Validation
With the received commands, the command validation will execute the business logic to verify if a certain action can take place. For example, if a user can really change their name, this is verified in the command validation. If this succeeds, the command validation will create the events that reflect the changes. These events are now being handed down to the storage layer for storage.
Events
Zitadel handles events in two ways. Events that should be processed in near real-time are processed by an in-memory pub-sub system. Some events can be handled asynchronously using the spooler.
Pub Sub
The pub-sub system's job is to keep a query view up-to-date by feeding a constant stream of events to the projections. Our pub-sub system built into Zitadel works by placing events into an in-memory queue for its subscribers. There is no need for specific guarantees from the pub-sub system. Since the SOR is the ES, everything can be retried without loss of data. In case of an error, an event can be reapplied in two ways:
- The next event might trigger the projection to apply the whole difference
- The spooler takes care of background cleanups in a scheduled fashion
The decision to incorporate an internal pub-sub system with no need for specific guarantees is a deliberate choice. We believe that the toll of operating an additional external service like an MQ system negatively affects the ease of use of Zitadel as well as its availability guarantees. One of the authors of Zitadel did their thesis to test this approach against established MQ systems.
Spooler
The spooler's job is to keep a query view up-to-date or at least ensure that it does not have too big a lag behind the Event Store. Each query view has its own spooler responsible for looking for the events relevant to generate the query view. It does this by triggering the relevant projection. Spoolers are especially necessary where someone can query datasets instead of single ids.
Each view can have exactly one spooler, but spoolers are dynamically leader-elected, so even if a spooler crashes it will be replaced in a short amount of time.
Projections
Projections are responsible for normalizing data for the query side or for analytical purposes. They generally work by being invoked either through a scheduled spooler or the pub-sub subscription. When they receive events, they will create their normalized object and then store this in the query view and its storage layer.
Queries
The query side is responsible for answering read requests on data. It has some unique requirements:
- It needs to be easy to query
- Short response times are a MUST (80% of queries below 100ms on the API server)
- Availability MUST be high, even during high loads
- The query view MUST be able to be persisted for most requests
When we classify this with the CAP theorem, we would choose Available and Performance but leave Consistent aside.
Query Handler
The query handler receives all read relevant operations. These can either be queries or simple getById calls. When receiving a query, it will proceed by passing this to the repository, which will call the database and return the dataset. If a request calls for a specific id, the call will, most of the time, be revalidated against the Event Store. This is achieved by triggering the projection to make sure that the last sequence of an id is loaded into the query view.
The query side has the option to dynamically check the Event Store for newer events on a certain id to ensure consistent responses without delay.
Query View
The query view is responsible for querying the storage layer with requests from the command handler. It is also responsible for executing authorization checks to see if a request is valid and can be answered.
Storage Layer
As Zitadel itself is built completely stateless, only the storage layer is needed to persist states. The storage layer of Zitadel is responsible for multiple tasks, such as:
- Guaranteeing strong consistency for the command side
- Guaranteeing good query performance for the query side
- Backup and restore operations for disaster recovery purposes
Zitadel currently supports PostgreSQL. Make sure to read our Production Guide before you decide on using one of them.
Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to the mirror guide to migrate to PostgreSQL.