Skip to content

Replace decision model builder with a Decision - #937

Merged
DavidBadura merged 1 commit into
4.0.xfrom
dcb-decision
Oct 16, 2026
Merged

DavidBadura merged 1 commit into
4.0.xfrom
dcb-decision

Conversation

@DavidBadura

@DavidBadura DavidBadura commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Handlers now get a Decision that remembers everything they read and appends the new events with the matching append condition, so nobody has to pass $state->appendCondition around anymore.

#[Handle]
public function __invoke(SubscribeStudentToCourse $command, Decision $decision): void
{
    $state = $decision->read([
        'courseExists' => new CourseExists($command->courseId),
        'courseCapacity' => new CourseCapacityProjection($command->courseId),
    ]);

    // ...

    $decision->append([new StudentSubscribedToCourse($command->studentId, $command->courseId)]);
}

read() takes a single projection (typed via the projection generic, no phpstan extension needed) or an array of projections, which are read with one query like build() did before. Reads can depend on earlier reads, so chained reads like "students of a course, then the courses of each student" are just another read().

To make that safe, after is now the head of the store at the first read instead of the last matching event. With the last matching event, combining reads either blocks every append (a later read saw newer events) or misses concurrent changes of the first read. This needs a new AppendStore::head().

The command bus resolves the Decision argument through a new optional ParameterResolver on ServiceHandlerProvider, the container wires it when dcb is enabled. Without our command bus, DecisionFactory::begin() starts a decision. DecisionModelBuilder, StoreDecisionModelBuilder and DecisionModel are removed.

On Postgres the array form is as fast as before (about 9 ms for five projections). Single reads cost one query each plus one MAX(id) per decision. The phpstan extension needs an update to type the array form as a shape.

A Decision collects everything a command reads and appends the new events
with a matching append condition, so handlers no longer pass the condition
around. Reads can depend on earlier reads (chained reads), and an array of
projections is still read with a single query.

The append condition now uses the head of the store at the first read as
`after` instead of the last matching event. With the last matching event,
combining several reads either blocks every append or misses concurrent
changes of the first read.

Command handlers get a new Decision per dispatch as second argument when
the command bus is configured with the DecisionParameterResolver, which the
container does if dcb is enabled.
@github-actions

Copy link
Copy Markdown

Hello 👋

here is the most recent benchmark result:

SplitStreamBench
================

+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                         | time (kde mode)                                     | memory                                     |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                 | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad10000Events () | 2.498ms (±0.00%)   | 2.496ms (±0.00%)   | +0.10%    | 35.775mb        | 36.296mb   | -1.43%      |
| benchSave10000Events () | 419.720ms (±0.00%) | 466.194ms (±0.00%) | -9.97%    | 35.966mb        | 36.431mb   | -1.28%      |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SimpleSetupTaggableStoreBench
=============================

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 1.004ms (±0.00%)   | 903.300μs (±0.00%) | +11.14%   | 36.313mb        | 36.313mb   | 0.00%       |
| benchLoad10000Events ()                | 57.199ms (±0.00%)  | 57.156ms (±0.00%)  | +0.08%    | 36.313mb        | 36.313mb   | 0.00%       |
| benchSave1Event ()                     | 1.134ms (±0.00%)   | 1.256ms (±0.00%)   | -9.75%    | 36.313mb        | 36.313mb   | 0.00%       |
| benchSave10000Events ()                | 292.432ms (±0.00%) | 275.472ms (±0.00%) | +6.16%    | 36.313mb        | 36.313mb   | 0.00%       |
| benchSave10000Aggregates ()            | 7.235s (±0.00%)    | 8.491s (±0.00%)    | -14.79%   | 36.313mb        | 36.313mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 4.787s (±0.00%)    | 4.766s (±0.00%)    | +0.44%    | 36.313mb        | 36.313mb   | 0.00%       |
| benchAppend1Event ()                   | 5.450ms (±0.00%)   | 1.316ms (±0.00%)   | +314.06%  | 36.313mb        | 36.313mb   | 0.00%       |
| benchAppend100Events ()                | 14.960ms (±0.00%)  | 6.422ms (±0.00%)   | +132.94%  | 36.313mb        | 36.313mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SubscriptionEngineBench
=======================

+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                               | memory                                     |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>  | Tag: base       | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 2.531s (±0.00%) | 2.547s (±0.00%) | -0.63%    | 48.302mb        | 48.302mb   | 0.00%       |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+

NoopSubscriptionEngineBench
===========================

+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                                   | memory                                     |
+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>    | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 78.086ms (±0.00%) | 79.778ms (±0.00%) | -2.12%    | 48.302mb        | 48.302mb   | 0.00%       |
+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+

SimpleSetupStreamStoreBench
===========================

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 901.500μs (±0.00%) | 900.100μs (±0.00%) | +0.16%    | 35.147mb        | 35.147mb   | 0.00%       |
| benchLoad10000Events ()                | 56.802ms (±0.00%)  | 57.300ms (±0.00%)  | -0.87%    | 35.147mb        | 35.147mb   | 0.00%       |
| benchSave1Event ()                     | 1.108ms (±0.00%)   | 4.153ms (±0.00%)   | -73.31%   | 35.147mb        | 35.147mb   | 0.00%       |
| benchSave10000Events ()                | 238.911ms (±0.00%) | 264.237ms (±0.00%) | -9.58%    | 35.147mb        | 35.147mb   | 0.00%       |
| benchSave10000Aggregates ()            | 7.832s (±0.00%)    | 11.920s (±0.00%)   | -34.30%   | 35.147mb        | 35.147mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 4.677s (±0.00%)    | 4.675s (±0.00%)    | +0.06%    | 35.147mb        | 35.147mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SubscriptionEngineBatchBench
============================

+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                                    | memory                                     |
+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>     | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 119.602ms (±0.00%) | 84.068ms (±0.00%) | +42.27%   | 35.727mb        | 35.727mb   | 0.00%       |
+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+

DynamicConsistencyBoundaryBench
===============================

+------------------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
|                                                | time (kde mode)                                   | memory                                     |
+------------------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                                        | Tag: <current>    | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+------------------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| benchDecisionModelSmall ()                     | 1.788ms (±0.00%)  | 1.421ms (±0.00%)  | +25.81%   | 13.862mb        | 13.862mb   | -0.00%      |
| benchDecisionModelComposite ()                 | 15.734ms (±0.00%) | 14.175ms (±0.00%) | +10.99%   | 13.862mb        | 13.862mb   | -0.00%      |
| benchDecisionModelSingleReads ()               | 19.338ms (±0.00%) | +0.00%            | 13.862mb  | +0.00%          |            |             |
| benchDecisionModel10000Events ()               | 73.546ms (±0.00%) | 76.866ms (±0.00%) | -4.32%    | 13.862mb        | 13.862mb   | -0.00%      |
| benchDecisionModel10000EventsWithCheckpoint () | 9.757ms (±0.00%)  | 10.017ms (±0.00%) | -2.60%    | 13.862mb        | 13.862mb   | -0.00%      |
| benchDecisionModelOnlyLastEvent ()             | 1.613ms (±0.00%)  | 1.169ms (±0.00%)  | +38.01%   | 13.862mb        | 13.862mb   | -0.00%      |
| benchDecideAndAppend ()                        | 23.239ms (±0.00%) | 28.932ms (±0.00%) | -19.68%   | 13.862mb        | 13.862mb   | -0.00%      |
| benchDecideAndAppendSingleReads ()             | 29.950ms (±0.00%) | +0.00%            | 13.862mb  | +0.00%          |            |             |
| benchAppendWithCondition ()                    | 3.110ms (±0.00%)  | 3.102ms (±0.00%)  | +0.24%    | 13.862mb        | 13.862mb   | -0.00%      |
+------------------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+

PersonalDataBench
=================

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 886.800μs (±0.00%) | 872.800μs (±0.00%) | +1.60%    | 37.009mb        | 37.009mb   | 0.00%       |
| benchLoad10000Events ()                | 135.055ms (±0.00%) | 135.218ms (±0.00%) | -0.12%    | 37.009mb        | 37.009mb   | 0.00%       |
| benchSave1Event ()                     | 2.168ms (±0.00%)   | 1.886ms (±0.00%)   | +14.97%   | 37.009mb        | 37.009mb   | 0.00%       |
| benchSave10000Events ()                | 330.782ms (±0.00%) | 349.308ms (±0.00%) | -5.30%    | 37.064mb        | 37.064mb   | 0.00%       |
| benchSave10000Aggregates ()            | 12.014s (±0.00%)   | 10.980s (±0.00%)   | +9.41%    | 37.009mb        | 37.009mb   | -0.00%      |
| benchSave10000AggregatesTransaction () | 8.599s (±0.00%)    | 8.620s (±0.00%)    | -0.24%    | 37.009mb        | 37.009mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SnapshotsBench
==============

+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                   | memory                                     |
+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>    | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| benchLoad10000EventsMissingSnapshot () | 57.054ms (±0.00%) | 57.338ms (±0.00%) | -0.49%    | 35.218mb        | 35.218mb   | 0.00%       |
| benchLoad10000Events ()                | 1.064ms (±0.00%)  | 1.042ms (±0.00%)  | +2.07%    | 35.218mb        | 35.218mb   | 0.00%       |
+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+

CommandToQueryBench
===================

+----------------+------------------+------------------+-----------+-----------------+------------+-------------+
|                | time (kde mode)                                 | memory                                     |
+----------------+------------------+------------------+-----------+-----------------+------------+-------------+
| subject        | Tag: <current>   | Tag: base        | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------+------------------+------------------+-----------+-----------------+------------+-------------+
| benchCreate () | 5.479ms (±0.00%) | 3.393ms (±0.00%) | +61.50%   | 5.061mb         | 5.061mb    | 0.00%       |
| benchUpdate () | 5.096ms (±0.00%) | 4.301ms (±0.00%) | +18.47%   | 5.757mb         | 5.757mb    | 0.00%       |
| benchBoth ()   | 8.482ms (±0.00%) | 7.372ms (±0.00%) | +15.06%   | 5.213mb         | 5.213mb    | 0.00%       |
+----------------+------------------+------------------+-----------+-----------------+------------+-------------+

This comment gets update everytime a new commit comes in!

@DavidBadura DavidBadura added the enhancement New feature or request label Oct 15, 2026
@DavidBadura DavidBadura added this to the 4.0.0 milestone Oct 15, 2026
@DavidBadura
DavidBadura merged commit 88eaac6 into 4.0.x Oct 16, 2026
41 of 43 checks passed
@DavidBadura
DavidBadura deleted the dcb-decision branch October 16, 2026 20:28
DavidBadura added a commit to patchlevel/event-sourcing-phpstan-extension that referenced this pull request Oct 18, 2026
patchlevel/event-sourcing#937 added the Decision for the dynamic
consistency boundary. Reading an array of projections now becomes an
array shape with the state type of each projection, a single projection
its state type. The state type is taken from the projection generic and
falls back to the return type of initialState().

ProjectionBuilder::build() is typed the same way again, it was dropped
together with the other DCB parts while they only existed in 4.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants