Skip to content

Architecture, versioning and parallel persistence

Strates is designed around one architectural rule:

Build the next relational state privately, then publish it by changing the release pointer.

This rule provides both versioning and a safe boundary for parallel persistence.

Versioning model

A business state is identified by three values:

scope + scopeId + buildStrate

Example:

catalog / shop-42 / strate-A
catalog / shop-42 / strate-B
catalog / shop-99 / strate-C

Several versions can coexist physically in the business tables, but released_strates selects only one active version for each (scope, scopeId) pair.

released_strates

catalog / shop-42 -> strate-B
catalog / shop-99 -> strate-C

Released queries always combine the scope identifier with the selected buildStrate. This prevents readers from mixing rows from different versions.

Publication is separate from persistence

A traditional in-place workflow couples writes and visibility:

update live parent
update live children
replace relations
update derived rows

The operation must usually remain inside one sufficiently broad transaction if readers must never observe the intermediate states.

Strates changes the workflow:

released A
    |
    +------------------------------ readers continue using A

candidate B
    |
    +-- write parent
    +-- write child branch 1
    +-- write child branch 2
    +-- write relations
    |
    +-- validate completion
    |
    +-- release pointer A -> B

The expensive work happens before publication. The final visibility change does not rewrite the business graph.

Parallelize by dependency level

A relational graph is a dependency graph, not necessarily one serial chain.

Example:

                         Catalog
                       /    |     \
                      /     |      \
               Settings  Products   Tags
                            \        /
                             \      /
                            ProductTag

A valid execution plan may be:

level 0: Catalog
level 1: Settings | Products | Tags      <- independent work may run concurrently
level 2: ProductTag                     <- waits for Products and Tags
level 3: release

The snapshot boundary means all level-1 writes remain invisible until the release step, even when several workers or coroutines persist them at the same time.

Important implementation detail

Strates does not turn persistMany() into a concurrent operation. The package intentionally keeps persistence explicit. The application chooses how to schedule independent tasks using its Swoole/OpenSwoole runtime, job system or other concurrency mechanism.

The required contract is simple:

  1. every task writes the same candidate buildStrate;
  2. foreign-key dependencies are respected;
  3. every required task reports success;
  4. release() is called only after all required work completes;
  5. any failed build remains unreleased.

Why this can improve persistence speed

For independent branches with durations A, B and C, a sequential workflow has an approximate elapsed time of:

A + B + C

When the branches are truly independent and the database can sustain the concurrency, the elapsed portion can approach:

max(A, B, C)

The complete workflow is still bounded by its critical dependency path. Foreign-key parents, join entities and contended rows cannot be parallelized arbitrarily.

Performance therefore depends on:

  • connection-pool capacity;
  • database CPU and I/O;
  • index and constraint cost;
  • lock contention;
  • batch size;
  • coroutine scheduling overhead;
  • the shape of the dependency graph.

Strates makes parallel execution safe from a reader visibility perspective; it does not remove normal database scalability limits.

Transactions still have a role

Snapshot publication does not replace transactions.

Use transactions when one persistence task needs local ACID semantics, for example when several dependent statements inside the same branch must succeed or fail together.

The architectural difference is that those local transactions do not need to cover the entire snapshot build solely to keep intermediate rows hidden from readers.

worker A -> local transaction -> build B rows
worker B -> local transaction -> build B rows
worker C -> local transaction -> build B rows

all workers successful
        |
        v
release B

This reduces the need for long-lived global transaction boundaries during large imports, catalog generation, pricing recalculation and other graph-building workloads.

Failure behavior

If one required task fails:

candidate B
  +-- products: OK
  +-- tags: FAILED
  +-- settings: OK

do not release B. The current pointer remains on A, so readers continue to receive the previous complete state.

The candidate can be marked failed:

$stratifiedPersist->markBuildAsFailed($strate);

and removed later by garbage collection according to the configured retention policy.

When this architecture is a good fit

Strates is useful when a group of related rows represents one logical version and users should observe either the previous complete state or the next complete state.

Typical workloads include:

  • catalog generation;
  • pricing and promotion publication;
  • configuration deployment;
  • availability planning;
  • large imports;
  • synchronization from external systems;
  • generated or denormalized read models.

For a simple independent CRUD row, ordinary in-place persistence is usually simpler.

Summary

versioned build
      |
      +-- persist dependency branches
      |       +-- sequential where required
      |       +-- concurrent where independent
      |
      +-- validate completion
      |
      +-- release one pointer
      |
      +-- collect obsolete versions later

The key performance property is not that Strates automatically writes faster. It is that versioning decouples visibility from persistence, allowing the application to parallelize independent transaction/persistence work without exposing partial relational state.