State Storage

Using vlingo/symbio to store key-value state.

The state storage type persists key-value pairs. The key is a business id and the value is the serialized state of an entity. You may also transactionally append DomainEvent instances, and other Source types, such as Command, along with states.

Configuring and Starting the StateStore

Every StateStore is implemented as an Actor. Thus, you must use the World or one of its Stage instances to create the StateStore. Before doing this there are a few dependencies you must create, including Configuration. These get sent in as parameters to the StateStore constructor.

Every entity type must be registered. This is how the StateStore knows the table name to use for a given entity type.

// using the class simple name
final String productStoreName = Product.class.getSimpleName();
StateTypeStateStoreMap.stateTypeToStoreName(Product.class, productStoreName);
// using the scheme name
final String productStoreName =
"tbl_vlingo_symbio_state_" +
Product.class.getSimpleName();
StateTypeStateStoreMap.stateTypeToStoreName(Product.class, productStoreName);

Further, each persistent entity and Source--DomainEvent or Command--should have a serialization and deserialization adapter.

import io.vlingo.symbio.EntryAdapterProvider;
EntryAdapterProvider provider = EntryAdapterProvider.instance(world);
// ProductDefinedAdapter is a io.vlingo.symbio.EntryAdapter
provider.registerAdapter(ProductDefined.class, new ProductDefinedAdapter());
// ProductStateAdapter is a io.vlingo.symbio.StateAdapter
provider.registerAdapter(ProductState.class, new ProductStateAdapter());

The following is the configuration used to create a new StateStore for a given database, which in this case is Postgres.

import io.vlingo.lattice.model.projection.ProjectionDispatcher;
import io.vlingo.lattice.model.projection.ProjectionDispatcher.ProjectToDescription;
import io.vlingo.lattice.model.projection.TextProjectionDispatcherActor;
import io.vlingo.symbio.store.common.jdbc.Configuration;
import io.vlingo.symbio.store.common.jdbc.postgres.PostgresConfigurationProvider;
import io.vlingo.symbio.store.dispatch.Dispatcher;
import io.vlingo.symbio.store.state.jdbc.StorageDelegate;
import io.vlingo.symbio.store.state.jdbc.postgres.PostgresStorageDelegate;
World world = World.startWithDefaults("product-service");
...
List<ProjectToDescription> descriptions =
Arrays.asList(new ProjectToDescription(UserProjectionActor.class, Events.class.getPackage()),
...);
Dispatcher dispatcher =
world.actorFor(Dispatcher.class, TextProjectionDispatcherActor.class, descriptions);
Configuration configuration =
PostgresConfigurationProvider.configuration(...);
StorageDelegate delegate =
new PostgresStorageDelegate(configuration, world.defaultLogger());
StateStore stateStore =
world.actorFor(StateStore.class, JDBCStateStoreActor.class,
dispatcher, delegate);

This creates a new Actor-based StateStore that is ready to receive and process persistence messages.

Writing State and Source Instances

Reading State Instances

Streaming Over Entry Instances of Source Types

Creating StateStore Database Tables

There are several implementations of the StateStore. One primary type is for JDBC over relational databases. There are also implementations for Amazon DynamoDB and Apache Geode. The following provides set up and configuration for each of these.

Using Relational Databases with JDBC

Although the StateStore can auto-create all necessary tables, you may want to pre-create the necessary tables. There will be a unique table for every entity type stored. Note the {0} parameter in the following table naming scheme. You will replace this parameter with your entity type name, which may be the class simple name. This must include the prefix "tbl_". For example, a class named Product would have the table named tbl_product.

More formally, you may want to use the pattern tbl_vlingo_symbio_state_{0}, the class simple name replacing the parameter. For the class named Product, your table would be named tbl_vlingo_symbio_state_product. Using this scheme visually documents that your table is specifically used by the vlingo/symbio StateStore.This is the table creation script for Postgres.

CREATE TABLE {0} (
s_id VARCHAR(128) PRIMARY KEY,
s_type VARCHAR(256) NOT NULL,
s_type_version INT NOT NULL,
s_data {1} NOT NULL,
s_data_version INT NOT NULL,
s_metadata_value TEXT NOT NULL,
s_metadata_op VARCHAR(128) NOT NULL
);

Note that the s_data column is created according to your preferred storage type, text or binary. The following table explains per database.

Database

Column Type

HSQLDB

The Text type is defaulted to LONGVARCHAR(65535).

HSQLDB

The Binary type is defaulted to VARBINARY(65535).

MariaDB

See MySQL.

MySQL

The Text type is defaulted to TEXT.

MySQL

The Binary type is defaulted to VARBINARY(4096).

Postgres

The Text type is defaulted to JSONB, making your entities' attributes searchable.

Postgres

The Binary type is defaulted to BYTEA.

YugaByte

See Postgres.

You indicate your preferences in the configuration.

// Configuration
package io.vlingo.symbio.store.common.jdbc.Configuration;
public class Configuration {
...
public final DataFormat format;
...
}

The DataFormat type is an enum with the following types.

// DataFormat
package io.vlingo.symbio.store;
public enum DataFormat {
Binary,
Text
};

The following format type and column type pair up: DataFormat.Binary and s_data column type BYTEA. Likewise the following two are used together:DataFormat.Text and s_data column type TEXT. You must consistently used the same types across all tables that use the same StateStore.

The following table must be created to support dispatching write events from the StateStore. This is how we ensure that Dispatchables are delivered via the Dispatcher.

CREATE TABLE tbl_vlingo_symbio_dispatchables (
d_id BIGSERIAL PRIMARY KEY,
d_created_at TIMESTAMP NOT NULL,
d_originator_id VARCHAR(32) NOT NULL,
d_dispatch_id VARCHAR(128) NOT NULL,
d_state_id VARCHAR(128) NOT NULL,
d_state_type VARCHAR(256) NOT NULL,
d_state_type_version INT NOT NULL,
d_state_data {1} NOT NULL,
d_state_data_version INT NOT NULL,
d_state_metadata_value TEXT NOT NULL,
d_state_metadata_op VARCHAR(128) NOT NULL,
d_state_metadata_object TEXT,
d_state_metadata_object_type VARCHAR(256),
d_entries TEXT
);
CREATE INDEX idx_dispatchables_dispatch_id
ON tbl_vlingo_symbio_dispatchables (d_dispatch_id);
CREATE INDEX idx_dispatchables_originator_id
ON tbl_vlingo_symbio_dispatchables (d_originator_id);

The same rules for the DataFormat and d_state_data column type apply for the tbl_vlingo_symbio_dispatchables table. You must consistently used the same types across all tables that use the same StateStore.

The following is the table used to store DomainEvent instances and other Source types such as Command.

CREATE TABLE tbl_vlingo_symbio_state_entry (
e_id BIGSERIAL PRIMARY KEY,
e_type VARCHAR(256) NOT NULL,
e_type_version INT NOT NULL,
e_data {1} NOT NULL,
e_metadata_value VARCHAR(4000) NOT NULL,
e_metadata_op VARCHAR(128) NOT NULL
);

The same rules for the DataFormat and d_state_data column type apply for the tbl_vlingo_symbio_state_entry table and its e_data column. You must consistently used the same types across all tables that use the same StateStore.

The following table supports totally ordered streaming reads of the Entry instances stored in the tbl_vlingo_symbio_state_entry table. The readers implement the interfaceStateStoreEntryReader<T>, which extends EntryReader<T>.

CREATE TABLE tbl_vlingo_symbio_state_entry_offsets (
reader_name VARCHAR(128) PRIMARY KEY,
reader_offset BIGINT NOT NULL
);

Each EntryReader<T> type has a name and a current offset, which are used to track the current position in the stream. The stream position/offset corresponds to a value of e_id in the tbl_vlingo_symbio_state_entry table.

Using Amazon DynamoDB

Using Apache Geode