Add post-append side-effect parity to the DCB application service - #210
Add post-append side-effect parity to the DCB application service#210johanhaleby wants to merge 3 commits into
Conversation
Qodana Community for JVM74 new problems were found
View the detailed Qodana reportTo be able to view the detailed Qodana report, you can either:
To get - name: 'Qodana Scan'
uses: JetBrains/[email protected]
with:
upload-result: trueContact Qodana teamContact us at [email protected]
|
There was a problem hiding this comment.
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/GenericDcbApplicationServicewith anexecute(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.
dfbd170 to
83bd86f
Compare
10d1781 to
b585ba8
Compare
513d5bd to
deb15b4
Compare
b585ba8 to
29a53d6
Compare
deb15b4 to
f144677
Compare
29a53d6 to
9f27a12
Compare
f144677 to
d38a5c3
Compare
0fa6865 to
f9e9d8a
Compare
d38a5c3 to
0392765
Compare
f9e9d8a to
ae8221e
Compare
50a590e to
b67f143
Compare
13fe7c9 to
b0c0598
Compare
8950668 to
a67a58c
Compare
4bac592 to
8cc4ac3
Compare
a67a58c to
0de806a
Compare
13f7b15 to
49975ee
Compare
0de806a to
6085089
Compare
49975ee to
b8eff0f
Compare
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]>
b8eff0f to
bcc192d
Compare
5cf9e6e to
84539c1
Compare
|
Landed via the consolidated #234. |
What
Gives the DCB application service post-append side-effect parity with the stream application service.
GenericApplicationServiceruns 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, withoptions()/empty()factories and asideEffect(...)builder.DcbApplicationServicegainsexecute(query, options, fn)as the primary overload. The existingexecute(query, fn)now delegates to it with empty options, so callers are unaffected.GenericDcbApplicationServicemirrors 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.PolicySideEffectis reused as-is, not duplicated. Kotlin gets a reifieddcbSideEffectbuilder (single policy and two-policy compose).The one intentional divergence from the stream options
DcbExecuteOptionshas no read-filter option, unlike the streamExecuteOptions. In DCB theDcbQuerypassed toexecuteis 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 reusedPolicySideEffectdiscriminates by event type, and the simpleexecute(query, fn)still appends.DcbExecuteOptionsExtensionsTest: the Kotlin reifieddcbSideEffectbuilder fires for the matching type and composes two policies.GenericApplicationServiceTestside-effect and retry tests pass unchanged (backward compat).