A real pipeline has many steps — fetch context, rebase, run tests, run AI assessments, judge output, generate a report. Passing every leaf step to a single top-level constructor produces an argument list that’s hard to read, hard to test, and hard to explain:
// 13 arguments — too much to take in at oncepublic PrReviewDslWorkflow( FetchPrContextStep fetchPrContext, RebaseStep rebaseStep, ConflictDetectionStep conflictDetection, RunTestsStep runTests, FixAndRetestStep fixAndRetestStep, CleanupStep cleanupStep, BuildGate buildGate, VersionPatternStep versionPatternStep, Step<PrContext, ?> assessCodeQuality, Step<PrContext, ?> assessBackport, QualityJudgeStep qualityJudgeStep, AssembleReportStep assembleReportStep, GenerateReportStep generateReport)
The fix is not cosmetic. The constructor is wrong at the level of abstraction — it describes leaves when it should describe structure.
The Spring @Configuration class is the wiring hub. It assembles each phase as a @Bean. Because Workflow<I, O> implements Step<I, O>, sub-workflows compose directly into parent workflows with no adapter needed.
Separation of concerns: the workflow is a structural description — it answers “what runs when.” The factory is the wiring layer — it answers “what object gets what dependency.” Mixing them produces the 13-argument constructor.Independent testability: each sub-workflow is a Workflow bean that can be tested in isolation with a minimal set of mock steps. You don’t need to construct all 13 collaborators to test the AI assessment phase:
// Test the AI assessment phase in isolationWorkflow<PrContext, Path> assessAndReport = new PrReviewConfig() .assessAndReport(versionPattern, mockAssessCode, mockAssessBackport, qualityJudge, assembleReport, generateReport);Path result = assessAndReport.execute(ctx, prContext);assertThat(result).exists();
Readable at every level: a reader of PrReviewDslWorkflow sees the four phases and the gate. A reader of PrReviewConfig.assessAndReport() sees the five steps. Neither method is overwhelmed by the other’s details.
This is the standard pattern across Java workflow and batch frameworks:
LangChain4j — @SequenceAgent(subAgents = {A.class, B.class}) references agents (phases), not the services inside them. The orchestrator doesn’t know how agent A is wired.
Google ADK Java — SequentialAgent.builder().addSubAgent(parallelAgent).build() where parallelAgent is already assembled. Leaf services stay inside sub-agents.
Spring Batch — Job references Step beans. A Step may contain an ItemReader, ItemProcessor, and ItemWriter, but the job definition never sees those — it sees only the step.
The consistent rule: the top-level orchestrator describes structure; the factory describes wiring.
The workflow constructor should be readable to anyone who wants to understand the pipeline. The configuration class should be readable to anyone who wants to understand how dependencies flow in.