Skip to content

Data Store

A Data Store is item-aware storage that outlives any single process instance and is shared across every instance in the running engine. Reach for it when a value must survive the instance that produced it — a counter, a cache, a hand-off between two otherwise unrelated processes. There are two halves: the engine-side store (datastore.DataStore, registered on the Thresher) and the flow-side reference (data_stores.DataStoreReference, an ItemAwareElement a task associates with, just like a Data Object). This page is the data-model reference — the interfaces, the reference type, and the real construction/read/write calls.

Taxonomy

BPMN category Data → Data Store / DataStoreReference (§10.4.1)
Store port github.com/dr-dobermann/gobpm/pkg/datastoreDataStore, Registry
Default adapter github.com/dr-dobermann/gobpm/pkg/datastore/memstoreStore, Registry
Flow reference github.com/dr-dobermann/gobpm/pkg/model/data_storesDataStoreReference
Embeds data.ItemAwareElement, flow.BaseElement
Registered via thresher.WithDataStore(ref, store)
Contrast a Data Object is per-instance, scope-resident, and dies with the instance

The store port

The engine holds each store behind the datastore.DataStore interface — item-aware data addressed by an opaque name. The default adapter is the in-memory memstore; a durable adapter is a swap-in behind this same interface.

type DataStore interface {
    Get(ctx context.Context, name string) (data.Data, bool, error)
    Put(ctx context.Context, name string, d data.Data) error
    Capacity() int
    IsUnlimited() bool
}
Member Role
Get(ctx, name) fetch the datum under name; the bool is false when none exists.
Put(ctx, name, d) store (or replace) d under name.
Capacity() nominal item capacity (§10.4.1) — advisory in memstore, a durable adapter may enforce it.
IsUnlimited() whether the store has no capacity bound.

A Registry resolves a store by its reference id. An unregistered ref is an error — fail-loud, since a reference to an unknown store is a configuration mistake, not a silent auto-provision:

type Registry interface {
    Store(ref string) (DataStore, error)
}

You rarely call Get/Put/Store yourself — the engine reroutes a task's data associations to the named store for you. You implement this interface only to supply a custom backing (see Custom Data Store).

The default adapter (memstore)

memstore is the non-durable, concurrency-safe in-memory store:

Symbol Signature Role
memstore.New New(opts ...Option) *Store build an in-memory store.
memstore.WithCapacity WithCapacity(n int) Option set a nominal (advisory) capacity.
memstore.Unlimited const Unlimited = 0 the default: no capacity bound.
memstore.NewRegistry NewRegistry() *Registry a registry of named stores, registered up front.

memstore.Store implements the full datastore.DataStore interface (Get/Put/Capacity/IsUnlimited); its capacity is advisory — a Put past a nominal capacity is not rejected.

The flow reference

A task never names a store directly; it associates with a DataStoreReference, a flow-scope ItemAwareElement that carries a dataStoreRef. Data flowing into/out of the reference flows into/out of the engine store named by that ref, keyed by the reference's Name().

func New(
    name, dataStoreRef string,
    idef *data.ItemDefinition,
    state *data.SrcState,
    baseOpts ...options.Option,
) (*DataStoreReference, error)
Parameter Meaning
name the reference name — also the key the value is stored under in the engine store.
dataStoreRef the store's registration id; two references with the same dataStoreRef point at one backing store.
idef the item definition (the value's shape).
state the initial data.SrcState (e.g. data.ReadyDataState).
baseOpts base-element options (id, docs).

The reference wires to a node through one of two association methods — the only difference between a writer and a reader:

Method Direction Effect
AssociateSource(n, sourceIDs, transformation, shape...) Node → DataStore binds node n's output (by source id) into the store — a DataOutputAssociation.
AssociateTarget(n, transformation, shape...) DataStore → Node binds the store into node n's input — a DataInputAssociation.
AssociateTargetInput(n, inputID, transformation, shape...) DataStore → Node the same, naming the input by its id — for a throw event (Event data).

All three take an optional data.FormalExpression transformation (nil for a straight copy) and the same shape tail every association accepts — data.WithAssignments(...), data.WithSources(...) — described under Data objects. An assignment whose target is a store reads the record first and writes back the whole of it, so the fields the assignment does not name survive. Introspection: DataStoreRef(), Name(), ID(), EType().

Build it

Register one in-memory store on the engine under a ref (here "shared"); call WithDataStore once per distinct store. Registering an already-used ref replaces the previous store.

eng, err := thresher.New("data-store-demo",
    thresher.WithDataStore("shared", memstore.New()))

The writer task carries an output parameter; a reference to "shared", named "counter", binds that output into the store via AssociateSource:

ref, err := datastores.New("counter", "shared", idef(), data.ReadyDataState)
if err != nil {
    return nil, err
}
if err := ref.AssociateSource(writer, []string{itemID}, nil); err != nil {
    return nil, err
}

The reader task, in its own process, names the same "shared" store and binds it to its input via AssociateTarget. Built the same way — only the association direction differs:

ref, err := datastores.New("counter", "shared", idef(), data.ReadyDataState)
if err != nil {
    return nil, err
}
if err := ref.AssociateTarget(reader, nil); err != nil {
    return nil, err
}

Inside the reader's Go function the value arrives by id, like any other input:

d, err := r.GetDataByID(itemID)
if err != nil {
    return nil, err
}
if v, ok := d.Value().Get(ctx).(int); ok {
    seen <- v
}

Run it

cd examples/data-store && go run .

The writer instance completes, then a separate reader instance — launched after the writer is done — reads back what the writer left in the store:

✓ writer instance stored 42 in DataStore "shared" key "counter"
✓ reader instance read 42 from the shared DataStore
✓ data-store demo: the value outlived the writer instance and crossed into the reader through the engine-global store

Runtime behavior

  • Cross-instance, cross-process. The reader runs in a different instance than the writer — from a different process — yet sees 42. The value crossed the instance boundary through the engine-global store. That is the whole point; a Data Object cannot do this.
  • Name is the key. The reference's Name() ("counter") is the store key; the dataStoreRef ("shared") selects the backing store. Distinct dataStoreRefs never share state.
  • Fail-loud resolution. A DataStoreReference whose dataStoreRef was never registered on the engine is a configuration error, not a silent no-op — the registry rejects the unknown ref.
  • Not per-instance. A DataStoreReference is not a DataObject. If you only need data for one instance's lifetime, use a Data Object or a process property — they are scoped and die with the instance.
  • Swappable backing. memstore.New() is in-memory and non-durable; a durable adapter behind the same datastore.DataStore interface swaps in under the same ref without touching process code — see Custom Data Store.

See also