Skip to content

Add post-append side-effect parity to the DCB application service - #210

Closed
johanhaleby wants to merge 3 commits into
johan/dcb-catchup-subscriptionfrom
johan/dcb-appservice-sideeffects
Closed

Add post-append side-effect parity to the DCB application service#210
johanhaleby wants to merge 3 commits into
johan/dcb-catchup-subscriptionfrom
johan/dcb-appservice-sideeffects

Conversation

@johanhaleby

@johanhaleby johanhaleby commented Jun 21, 2026

Copy link
Copy Markdown
Owner

What

Gives the DCB application service post-append side-effect parity with the stream application service. GenericApplicationService runs a side-effect on the newly written events after a successful write, so a command can trigger a policy. The DCB application service had no equivalent, so a DCB command could not.

This is PR5 in the DCB stack.

How

  • DcbExecuteOptions<E> carries an optional post-append side-effect, with options() / empty() factories and a sideEffect(...) builder.
  • DcbApplicationService gains execute(query, options, fn) as the primary overload. The existing execute(query, fn) now delegates to it with empty options, so callers are unaffected.
  • GenericDcbApplicationService mirrors the stream timing exactly: read, domain function, convert, tag, and append run inside the retry, and the side-effect runs once after the retry succeeds, with the newly written domain events. It does not run on the no-new-events path and not per retry attempt.
  • The store-agnostic PolicySideEffect is reused as-is, not duplicated. Kotlin gets a reified dcbSideEffect builder (single policy and two-policy compose).

The one intentional divergence from the stream options

DcbExecuteOptions has no read-filter option, unlike the stream ExecuteOptions. In DCB the DcbQuery passed to execute is both the read filter and the consistency boundary, so a separate filter here would be redundant and misleading. The Javadoc states this so the asymmetry is not mistaken for an oversight.

Decider extensions

Left as-is. A decider caller that wants a side-effect uses execute(query, options, fn) with a decider-built function, which avoids multiplying the decider overload surface (decision, state, events, single and list command) for marginal value. Flagging this as a deliberate scope call.

Tests

  • DcbApplicationServiceSideEffectTest: the side-effect runs once with the written events, does not run when the domain function produces no events, a reused PolicySideEffect discriminates by event type, and the simple execute(query, fn) still appends.
  • DcbExecuteOptionsExtensionsTest: the Kotlin reified dcbSideEffect builder fires for the matching type and composes two policies.
  • The stream GenericApplicationServiceTest side-effect and retry tests pass unchanged (backward compat).

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown

Qodana Community for JVM

74 new problems were found

Inspection name Severity Problems
Kotlin Maven Plugin misconfigured 🔶 Warning 27
Constant values 🔶 Warning 25
Nullability and data flow problems 🔶 Warning 14
'Optional' used as field or parameter type 🔶 Warning 2
Pointless boolean expression 🔶 Warning 2
'equals()' called on classes which don't override it 🔶 Warning 1
Result of method call ignored 🔶 Warning 1
Function or property has platform type ◽️ Notice 2
View the detailed Qodana report

To be able to view the detailed Qodana report, you can either:

To get *.log files or any other Qodana artifacts, run the action with upload-result option set to true,
so that the action will upload the files as the job artifacts:

      - name: 'Qodana Scan'
        uses: JetBrains/[email protected]
        with:
          upload-result: true
Contact Qodana team

Contact us at [email protected]

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds post-append side-effect support to the blocking DCB application service so DCB commands can trigger policies after a successful append, matching the stream-based application service’s side-effect timing semantics.

Changes:

  • Introduces DcbExecuteOptions<E> carrying an optional post-append side-effect, plus Kotlin convenience builders (dcbExecuteOptions, dcbSideEffect).
  • Extends DcbApplicationService/GenericDcbApplicationService with an execute(query, options, fn) overload and runs the side-effect once after a successful append (not on no-new-events, not per retry).
  • Adds Java/Kotlin tests validating side-effect behavior and typed policy composition.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
changelog.md Documents the new DCB post-append side-effect option and its intended semantics.
application/service/blocking/src/main/java/org/occurrent/application/service/blocking/dcb/DcbApplicationService.java Adds the options-based execute overload and delegates from the simpler overload.
application/service/blocking/src/main/java/org/occurrent/application/service/blocking/dcb/GenericDcbApplicationService.java Implements side-effect execution once after successful retry+append, returning Optional.empty() on no-new-events.
application/service/blocking/src/main/java/org/occurrent/application/service/blocking/dcb/DcbExecuteOptions.java New options type holding an optional post-append side-effect (no read-filter by design).
application/service/blocking/src/main/kotlin/org/occurrent/application/service/blocking/dcb/DcbExecuteOptionsExtensions.kt Kotlin helpers for creating options and typed PolicySideEffect side-effects.
application/service/blocking/src/test/java/org/occurrent/application/service/blocking/dcb/DcbApplicationServiceSideEffectTest.java Verifies side-effect runs once with newly appended events and not on no-new-events.
application/service/blocking/src/test/kotlin/org/occurrent/application/service/blocking/dcb/DcbExecuteOptionsExtensionsTest.kt Tests Kotlin dcbSideEffect typed filtering and composition.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from 10d1781 to b585ba8 Compare June 22, 2026 05:39
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from 513d5bd to deb15b4 Compare June 22, 2026 05:45
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from b585ba8 to 29a53d6 Compare June 22, 2026 05:45
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from deb15b4 to f144677 Compare June 22, 2026 10:27
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from 29a53d6 to 9f27a12 Compare June 22, 2026 10:27
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from f144677 to d38a5c3 Compare June 24, 2026 15:12
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch 2 times, most recently from 0fa6865 to f9e9d8a Compare June 24, 2026 17:52
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from d38a5c3 to 0392765 Compare June 24, 2026 17:52
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from f9e9d8a to ae8221e Compare June 25, 2026 05:07
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch 2 times, most recently from 50a590e to b67f143 Compare June 25, 2026 12:05
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch 2 times, most recently from 13fe7c9 to b0c0598 Compare June 26, 2026 10:31
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch 2 times, most recently from 8950668 to a67a58c Compare June 26, 2026 11:22
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from 4bac592 to 8cc4ac3 Compare June 26, 2026 11:22
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from a67a58c to 0de806a Compare June 26, 2026 11:37
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch 2 times, most recently from 13f7b15 to 49975ee Compare June 29, 2026 06:30
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from 0de806a to 6085089 Compare June 29, 2026 06:30
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from 49975ee to b8eff0f Compare June 29, 2026 09:59
johanhaleby and others added 3 commits June 29, 2026 12:12
GenericApplicationService runs a side-effect on the newly written events
after a successful write. The DCB application service had no equivalent, so
a DCB command could not trigger a policy after its append.

Adds DcbExecuteOptions carrying an optional post-append side-effect and a
new execute(query, options, fn) overload (the simple execute(query, fn) now
delegates to it with empty options). GenericDcbApplicationService mirrors
the stream timing exactly: read, domain function, convert, tag, and append
run inside the retry, and the side-effect runs once after the retry
succeeds with the newly written domain events. It does not run on the
no-new-events path and not per retry attempt.

DcbExecuteOptions deliberately has no read-filter option. In DCB the
DcbQuery passed to execute is both the read filter and the consistency
boundary, so a separate filter would be redundant and misleading. This is
the one place the DCB options intentionally diverge from the stream
ExecuteOptions, and the Javadoc says so.

The store-agnostic PolicySideEffect is reused as-is rather than duplicated.
Kotlin gets a reified dcbSideEffect builder (single and two-policy compose)
in DcbExecuteOptionsExtensions.kt. The decider extensions are left as-is: a
decider caller that wants a side-effect uses execute(query, options, fn)
with a decider-built function, which avoids multiplying the decider overload
surface.

Tests: DcbApplicationServiceSideEffectTest (side-effect runs once with the
written events, does not run when no events are produced, a reused
PolicySideEffect discriminates by type, the simple overload still appends)
and DcbExecuteOptionsExtensionsTest (Kotlin reified builder fires and
composes). The stream GenericApplicationServiceTest side-effect and retry
tests pass unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Java DcbApplicationService.execute returns Optional<DcbAppendResult>,
which is correct for Java (empty signals a no-op command that appended
nothing, since DcbAppendResult only carries assigned positions). In Kotlin
that Optional is awkward.

Adds executeSequence and executeList extensions on DcbApplicationService
that return a nullable DcbAppendResult? (null on a no-op command) and take
an optional DcbExecuteOptions, mirroring the stream executeSequence and
executeList. The names sidestep an overload clash with the Java execute
that takes a Function. The Kotlin decider execute extensions now return
DcbAppendResult? as well, instead of Optional<DcbAppendResult>.

Net effect: Java keeps the honest Optional, Kotlin never sees it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The DcbApplicationService decider execute extensions now accept a decider
whose event type is a subtype of the service's event type and widen it via
adaptEvents, so a feature decider over its own narrow event type can be
passed straight to an injected DcbApplicationService over the whole
domain's event type without calling adapt/adaptEvents first.

The overloads become inline+reified and constrain the command type to
non-null (C : Any), which commands always are.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@johanhaleby
johanhaleby force-pushed the johan/dcb-appservice-sideeffects branch from b8eff0f to bcc192d Compare June 29, 2026 10:12
@johanhaleby
johanhaleby force-pushed the johan/dcb-catchup-subscription branch from 5cf9e6e to 84539c1 Compare June 29, 2026 10:12
@johanhaleby

Copy link
Copy Markdown
Owner Author

Landed via the consolidated #234.

@johanhaleby
johanhaleby deleted the johan/dcb-appservice-sideeffects branch June 29, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants