Skip to main content

Online edition for BLOGE 0.9.9-RC1 · prepared 2026-11-02 · 中文

Chapter 34 — Labs

Promise: By the end of this chapter you will have completed foundation, advanced, and graduation capstones. The final artifact includes not only a graph, but technical tests, a business Scenario/Policy, evidence interpretation, an architecture diagram, and a list of unproven claims.


Learning Goals​

  1. Evolve a graph from a bounded starter instead of guessing syntax in an empty file.
  2. Deliver separate evidence for the graph contract, business contract, and release boundary.
  3. Run one-variable failure experiments for effects, waiting, recovery, and fallback.
  4. Explain the runtime boundary in one architecture diagram and the evidence boundary in one receipt-reading diagram.
  5. State what is proved, what is unproved, and what still needs human approval.

Prerequisites​

All thirty-three earlier chapters. The labs assume you are comfortable with:

  • Graph structure, dependencies, and data flow (Ch 1–Ch 4)
  • Branches and resilience (Ch 5–Ch 6)
  • Operator design and DSL authoring (Ch 7–Ch 8)
  • Subgraphs, iteration, waiting, and durability (Ch 10–Ch 13)
  • Orchestration patterns — sessions, state machines, and nested lifecycle ownership (Ch 14–Ch 16)
  • Tooling and review feedback (Ch 9)
  • Testing, production wiring, and scale (Ch 18–Ch 22)
  • Business correctness, evidence, and release claims (Ch 23–Ch 32)
  • Migration thinking (Ch 33)

Source Examples​

FileWhat it shows
order-process.blogeFoundation start — fan-out, fan-in, branch, resilience
ticket-routing.blogeAdvanced start — sentiment analysis and multi-way branch
loan-approval.blogeGraduation start — loan business and verification assets
batch-order-parallel.blogeOptional batch exercise — parallel foreach
status-polling.blogeOptional loop exercise — until and loopIteration
payment-wait.blogeOptional await reference — correlation and timeout
order-saga.blogeGraduation reference — saga compensation
GraphTestRunner.javaTest harness used in every lab's verify step
MockOperator.javaTest doubles — returning, throwing, delaying, recording
DslTestHelper.javaDSL compile + expectParseError / expectCompileError
missing-timeout.blogeAntipattern reference — nodes without a timeout budget
over-broad-fallback.blogeAntipattern reference — fallback that masks real failure modes

Why This Matters​

Reading about graphs is not the same as designing them. Every earlier chapter introduced one concept at a time. Real workflows combine all of them at once:

  • An order pipeline needs fan-out and resilience and branching.
  • A ticket router needs sentiment analysis and a three-way branch and a fallback for a flaky ML service.
  • A batch processor needs foreach and downstream aggregation and tests that assert on each item's outcome.

These labs test whether the concepts compose in practice. Each lab starts from a real conformance fixture already in the repository, so you are never inventing syntax. You are reading, extending, and testing.


Mental Model​

Think of the three capstones as progressively wider responsibility:

Diagram: 34-labs figure 1

Read — Open the conformance fixture. Trace every dependency. Identify which nodes run in parallel and which wait.

Extend — Add the features the lab asks for. Wire inputs from upstream outputs. Choose resilience policies deliberately.

Verify — Use GraphTestRunner and MockOperator to execute the graph, assert on NodeStatus values, and check execution order.

Diagram: three levels of capstone responsibility

LevelExpected effortStarting fileWork contractCompletion standard
Foundation60–90 minutesorder-process.blogeOne DSL copy + graph tests; explicit stepsSuccess and rejection paths are reviewable
Advanced2–3 hoursticket-routing.blogeDSL, mocks, listener test; boundary hints onlyFallback and observation facts stay distinct
Graduation1–2 daysLoan starter + verification assetsGraph, tests, Scenario/Policy/Fixture, diagrams, report; acceptance contract onlyRunnable, explainable, reviewable, with unproven claims

Copy each read-only starter into your own work directory. Do not modify submodule/examples or submodule/bloge; they are the remote example baseline and the RC1 source of truth respectively.


First Working Example​

Before you start the labs, make sure you can run the simplest possible test-driven graph cycle. This is your warm-up.

// 1. Compile DSL
var helper = new DslTestHelper(registry);
Graph graph = helper.compile("""
graph warmup {
node greet : GreetOperator {
input { name = ctx.name }
}
}
""");

// 2. Wire a mock operator
var runner = new GraphTestRunner(Map.of(
"GreetOperator", MockOperator.returning(Map.of("message", "Hello"))
));

// 3. Execute
var ctx = new GraphContext(Map.of("name", "Lab Runner"));
GraphResult result = runner.execute(graph, ctx);

// 4. Assert
assert result.isSuccess();
runner.assertNodeExecuted("greet");

If that compiles and passes, you are ready.


Break It Apart​

Every lab follows the same anatomy. Here is what each piece teaches:

PhaseSkill exercisedChapter callback
Read the fixtureDependency tracing, parallel identificationCh 3
Add a nodeOperator binding, input wiring, depends_onCh 2, Ch 4
Add resilienceretry, timeout, fallback, compensateCh 6
Add a branchbranch on, otherwise, skipped-node reasoningCh 5
Add iterationforeach, loop, carry, untilCh 11
Add suspensionawait, event correlation, on_timeoutCh 12
Write testsGraphTestRunner, MockOperator, DslTestHelperCh 18

Common Trap​

❌ Adding resilience to every node​

It is tempting to slap retry + timeout + fallback on every node in the graph. Don't.

Study the antipattern in over-broad-fallback.bloge: a blanket fallback on authorizePayment hides validation errors behind the same "manual review" as transient timeouts. Downstream nodes cannot distinguish a legitimate degradation from a permanent bug.

And study missing-timeout.bloge: a node calling an external service with no timeout at all can stall the entire graph indefinitely.

Rule of thumb for the labs:

  • External calls → always timeout.
  • Transiently flaky calls → retry with timeout per attempt.
  • Only add fallback when partial data is genuinely acceptable to every downstream consumer.
  • Pure local computation (transforms, aggregation) → no resilience needed.

Guided Rewrite​

Before diving into the full labs, practice the extend-and-verify cycle on a small change.

Open order-process.bloge. Currently createOrder depends only on calcPrice:

node createOrder : CreateOrderOperator {
depends_on = [calcPrice]
input {
user = fetchUser.output
price = calcPrice.output
}
}

Rewrite task: The business now requires that createOrder also logs the credit-check result for audit purposes — but only when the order is approved. Add a new input binding:

node createOrder : CreateOrderOperator {
depends_on = [calcPrice, checkCredit]
input {
user = fetchUser.output
price = calcPrice.output
creditCheck = checkCredit.output
}
}

Now verify the change is safe:

var runner = new GraphTestRunner(Map.of(
"FetchUserOperator", MockOperator.returning(Map.of("id", "u1")),
"FetchProductsOperator", MockOperator.returning(Map.of("items", List.of())),
"CalcPriceOperator", MockOperator.returning(Map.of("total", 99.0)),
"CreditCheckOperator", MockOperator.returning(Map.of("approved", true)),
"CreateOrderOperator", MockOperator.recording(),
"RejectOrderOperator", MockOperator.recording()
));

var ctx = new GraphContext(Map.of(
"userId", "u1",
"productIds", List.of("p1")
));

GraphResult result = runner.execute(graph, ctx);
assert result.isSuccess();
runner.assertNodeExecuted("createOrder");
runner.assertNodeSkipped("rejectOrder");

This pattern — extend the graph, then write a test that proves the new wiring — is the core muscle these labs build.


Brain Check​

Before you start the labs, answer these without looking back:

  1. In ticket-routing.bloge, which nodes run in parallel? (Answer: fetchCustomer and fetchTicketHistory — neither depends on the other.)

  2. In loan-approval.bloge, what is the maximum parallelism after fetchApplication completes? (Answer: four — checkCredit, detectFraud, verifyIncome, and checkBlacklist all depend only on fetchApplication.)

  3. In batch-order-parallel.bloge, can summarize start before every foreach iteration has finished? (Answer: no — summarize depends on processOrders, which completes only when all iterations are done.)

  4. What MockOperator factory would you use to simulate a node that takes 500 ms and then succeeds? (Answer: MockOperator.delaying(Duration.ofMillis(500), output).)

  5. What GraphTestRunner method proves a branch target was not chosen? (Answer: assertNodeSkipped("nodeId").)


Lab​

Foundation Capstone — Read and Extend an Order Graph​

Expected effort: 60–90 minutes
Starting file: order-process.bloge, copied before editing
Allowed scope: one DSL copy and one GraphTestRunner suite; do not edit operator implementations
Hint level: explicit steps

Requirements:

  1. Add a fetchInventory node that runs in parallel with fetchUser and fetchProducts. Input: ctx.productIds. Timeout: 2s.
  2. Wire fetchInventory.output into calcPrice as a new inventory input. Update depends_on accordingly.
  3. Add a compensate block to createOrder that calls ReleaseInventoryOperator — modelled after the pattern in order-saga.bloge.
  4. Verify: Write a test that asserts fetchInventory, fetchUser, and fetchProducts all complete; that createOrder executes when credit is approved; and that rejectOrder is skipped.

Completion standard: the real compiler accepts the DSL; approved and rejected paths separately assert EXECUTED/SKIPPED; an inventory timeout changes only the expected node; include one sentence saying this lab did not prove the business rule correct.


Advanced Capstone — Make Ticket Degradation Observable​

Expected effort: 2–3 hours
Starting file: ticket-routing.bloge, copied before editing
Allowed scope: DSL, mock operators, and one listener test; do not call a real ticket system
Hint level: boundaries, not complete DSL

Requirements:

  1. analyzeSentiment already has a fallback. Add retry = { attempts: 2, backoff: 300ms, strategy: jitter } to fetchTicketHistory so it can survive transient history-service failures.
  2. Add an escalateToManager node as a fourth branch case for classifyPriority.output.priority == "critical". Input: customerId from fetchCustomer.output.id and sentiment from analyzeSentiment.output.
  3. Verify: Write one test where classifyPriority returns { priority: "critical" } and assert that escalateToManager executes while assignVipAgent, assignNormalAgent, and autoResolve are all skipped.

Completion standard: critical, normal, and fallback paths are reviewable; metric, log, and node status are distinct under one executionId; fallback success is not described as business success.


Graduation Capstone — Deliver a Reviewable Loan Change​

Expected effort: 1–2 days
Starting files: loan-approval.bloge plus the Scenario/Policy/Fixture test assets under that module's src/test, copied before editing
Allowed scope: graph, controlled operator doubles, Scenario, Policy, Fixture, evidence interpretation, and diagrams; do not approve GOLDEN on behalf of a business owner or call a real disbursement effect
Hint level: acceptance contract only

Requirements:

  1. Add a reserveFunds node between aggregateRisk and makeDecision. reserveFunds depends on aggregateRisk and takes application = fetchApplication.output and risk = aggregateRisk.output as input.
  2. Give reserveFunds an output { reservationId: String } declaration and a compensate block calling ReleaseFundsOperator — the same saga pattern used in order-saga.bloge.
  3. Update makeDecision so it also depends_on reserveFunds.
  4. Add timeout = 3s to reserveFunds and fallback = { reservationId: "NONE", status: "degraded" } so a slow reserve doesn't block the pipeline.
  5. Technical verification: Write a test with a MockOperator.throwing(...) for ReserveFundsOperator that proves the fallback fires and the graph still reaches makeDecision.
  6. Business verification: Cover at least auto-approve, manual-review, and reject; forbid real effects in Policy and fix credit, fraud, and income facts in Fixture.
  7. Evidence interpretation: Write separate conclusions for verdict, evidence trust/source binding, Requirement contribution, and release claim.
  8. Architecture: Submit one graph/runtime/effect boundary diagram and one evidence-to-claim diagram, naming the business owner and release owner.
  9. Unproven claims: Include production capacity, real downstream health, GOLDEN approval, and release authorisation.

Diagram: the six graduation deliverables

Completion standard: all six asset classes exist; a one-variable failure is reproducible; every report statement points to a Scenario, source test, or human responsibility; no PASS is expanded into release approval.


Optional Extension Bank​

The next three exercises do not count toward the three-capstone completion standard. They are workshop extensions. Answers and grading notes live in the teacher guide so readers do not mistake six exercises for six graduation gates.

Batch Orders: Add Per-Item Resilience and Summary Branching​

Fixture: batch-order-parallel.bloge

Requirements:

  1. Add retry = { attempts: 1, backoff: 100ms, strategy: exponential } and timeout = 3s to the deductStock node inside the foreach body.
  2. Add a fallback = { deducted: false, reason: "stock service unavailable" } to deductStock so a single stock failure doesn't break the whole batch.
  3. After summarize, add a branch on summarize.output.allSucceeded:
    • true → notifySuccess (a new NotifySuccessOperator node)
    • false → notifyPartialFailure (a new NotifyPartialFailureOperator node)
  4. Verify: Wire deductStock as MockOperator.throwing(...) for one item to confirm the fallback fires, then assert notifyPartialFailure executes and notifySuccess is skipped.

Concepts exercised: foreach, per-item resilience, fallback inside iteration, downstream branching on aggregate output (Ch 5, 6, 10).

Stretch task — global throttling and per-item failure modes. On top of the requirements above, set batch_size = 10 on the foreach so at most 10 items run concurrently, and set on_item_failure = continue so the batch never aborts on a single bad item. Use MockOperator.throwing(...) to make 3 of 20 items fail, then assert that:

  1. deductStock is invoked exactly 20 times (each item attempted).
  2. summarize.output.failedCount == 3 and allSucceeded == false.
  3. notifyPartialFailure runs once; notifySuccess is skipped.
  4. With batch_size = 10, the test observes at most 10 concurrent deductStock invocations (wrap MockOperator.of(...) with an AtomicInteger active counter and update a second peak counter on entry).

This combination — batch_size + on_item_failure = continue + downstream branch on the aggregate — is the production pattern for resilient batch processing.


Status Polling to Event-Driven: Replace Loop with Await​

Fixtures: status-polling.bloge (start) and payment-wait.bloge (reference)

Requirements:

This lab has two parts.

Part A — Extend the polling loop:

  1. Add a notifyReady node after fetchResult that sends a notification when the result is available. Input: result = fetchResult.output.
  2. Add timeout = 5s to checkStatus inside the loop body.
  3. Verify: Assert notifyReady executes after fetchResult.

Part B — Replace the loop with await:

  1. Rewrite the graph to use await instead of a polling loop. After submitJob, declare:
    await awaitJobReady {
    event "job.ready" where jobId = submitJob.output.jobId
    timeout = 60s
    on_timeout {
    status = "timeout"
    reason = "Job did not become ready within 60 seconds"
    }
    }
  2. Add a branch on awaitJobReady.output.status:
    • "ready" → fetchResult
    • otherwise → handleTimeout (a new node that logs the failure)
  3. Verify: Write two tests — one that simulates the event arriving (assert fetchResult executes, handleTimeout skipped) and one that simulates a timeout (assert handleTimeout executes, fetchResult skipped).

Concepts exercised: loop, await, event correlation, on_timeout, suspension branching, contrasting polling vs. event-driven design (Ch 11, 11, 12).


Credit Approval Decision Table​

Goal: Refactor a branch-based credit-tier classifier into a decision_table node, then explore multi-policy tables and runtime error paths.

Step 1 — Baseline: the branch stack. Open (or create) ch22/credit-approval-baseline.bloge. The graph uses a branch on applicant.output.score with three explicit cases and an otherwise. Confirm it passes all existing tests.

Step 2 — Refactor to hit=unique with named output. Replace the branch with:

decision_table credit_tier(
score = applicant.output.score
) hit=unique -> { tier: String, rate: Decimal } {
rule (score: score >= 750) -> { tier: "platinum", rate: 3.25 }
rule (score: 680 <= score < 750) -> { tier: "gold", rate: 4.50 }
otherwise -> { tier: "rejected", rate: 0.0 }
}

Update downstream wiring to use credit_tier.output.tier and credit_tier.output.rate. Run tests — all should still pass.

Step 3 — Add hit=collect for discount accumulation. Add a second table that collects all applicable discount labels:

decision_table applicable_discounts(
score = applicant.output.score,
amount = loan.output.amount
) hit=collect -> String {
rule (score: score >= 700) -> "loyalty"
rule (score: score >= 760) -> "premium"
rule (amount: amount <= 100000) -> "small-loan"
}

Wire applicable_discounts.output.items to the notification node. Write a test asserting that score=780, amount=80000 returns three discount labels.

Step 4 — Trigger error paths. Write two negative test cases:

  1. Remove otherwise from the hit=unique table, then pass score=500. Assert that the graph throws DecisionTableViolationException with code RUNTIME_DECISION_TABLE_NO_MATCH.
  2. Add an overlapping rule score >= 700 alongside the existing score >= 750 rule (both resolve for score=800). Assert that the engine throws RUNTIME_DECISION_TABLE_AMBIGUOUS_MATCH with hit=unique. Restore the table after verifying.

Step 5 — BLOGE lint demo. Remove the otherwise clause from the hit=unique table and run bloge-lint (or observe the editor warning). Confirm the decision-table/missing-otherwise WARNING appears. Re-add otherwise to make the lint pass clean.

Exit Criteria:

  • hit=unique + named output: all tier + rate assertions pass.
  • hit=collect: discount accumulation test passes (3 labels for score=780, amount=80000).
  • Both error-path tests assert the correct code string.
  • bloge-lint reports zero WARNINGs after otherwise is restored.

Concepts exercised: decision_table, hit policies (unique, collect), named-field output, RUNTIME_DECISION_TABLE_NO_MATCH, RUNTIME_DECISION_TABLE_AMBIGUOUS_MATCH, decision-table/missing-otherwise lint rule, DecisionTableViolationException assertion (Ch 5, Appendix F).


Graduation Run and Evidence Check​

The graduation capstone must finish with a story defined by a business owner. From the pinned BLOGE 0.9.8-RC1 source tree, run the baseline:

mvn -pl bloge-starter-loan-approval -am \
-Dtest='LoanApprovalApplicationTest,LoanApprovalBusinessScenarioTest,LoanApprovalOperatorFlowTest' \
-Dsurefire.failIfNoSpecifiedTests=false test

The expected scope is three test classes and eleven tests: three decision paths plus funding success, retry, fallback, and compensation behavior. Treat that result as focused starter evidence only. Then run your graduation variant and record the test count, Scenario verdict, evidence trust, and unproven claims. Use Chapter 24 to inspect the business mapping and Chapter 23 to state what the green run still does not prove.

The complete reference decisions, common failures, and 20-point rubric are in solutions/ch34/teacher-guide.md. Open it only after the learner submits all six artifact classes; answers are not placed beside the tasks.


Experiment acceptance card​

  • Expected and observed: The capstone moves from parser to loan cases to evidence governance and six deliverables.
  • Failure and recovery: Fail a baseline, case, or evidence binding; repair that layer only and do not advance while red.
  • Proof boundary: Proves a learner variant meets the graduation contract, not release approval.
  • Exercise contract: First 33 chapters and fixed RC1; add one capability class per level; deliver six assets; stop when 91 baseline tests and the level rule pass.

Coding practices workshop​

Follow one loan application: credit 760, fraud 10, strong income, and a blacklist hit. The starter's existing teaching policy requires auto_reject. Move policy from a domain method into a decision table while preserving routing, seven risk-output fields, and the absence of unselected actions.

This chapter owns the complete assets:

Act one: predict without changing expected values​

Find blacklist-priority in the CSV. Rejection and approval predicates can both match. Explain rejection priority and identify the action that must not occur. These fixed answers describe the teaching policy; the refactored implementation does not generate them. Real policy changes need a new business-approved Oracle.

Act two: choose representation and ownership​

Use existing trim for name cleanup and application function loanText.normalizePolicyCode for locale-independent policy codes. The function does not read policy or approve loans. Keep an independently controlled credit query in an Operator, calculate policy in the decision table, and let the branch select actions.

Complete the Appendix J choice card: expression, execution responsibility, maintenance scope, rejected alternatives, and verification. Repeated code alone does not justify a new built-in or publishing bank policy in core.

Act three: replace one boundary and preserve the contract​

From the repository root, using Java 25 and Maven 3.9+:

# Install the candidate source dependencies needed by this workshop; upstream tests are skipped.
mvn -q -f submodule/bloge/pom.xml -pl bloge-dsl,bloge-durable,bloge-session-durable -am \
-DskipTests -Dinvoker.skip=true install

# Compile and execute this chapter's workshop.
mvn -f chapters/34-labs/assets/coding-practices/pom.xml test

The BLOGE checkout must match this edition's candidate source. Passing results from older artifacts are not interchangeable. This standalone Maven project needs no real credit provider, database, or funding account.

Twelve parameterized examples execute both graphs and compare all seven risk fields, including decision, priority, reasons, and review form. The refactor does not delete the aggregation responsibility that supplies metadata. Simulated action counts are observed independently of node status: a rejected case requires approve SKIPPED and zero approval actions.

Act four: attack the choice with counterexamples​

The execution stage is in FundingBoundaryTest. A visible credit query completes before funding. The provider commits once, loses its first response, and receives a retry with the same business key; the second call returns the original receipt. One configured graph retry produces two total calls in this API.

Another test commits a reservation and withholds the response until timeout. The graph fails while the provider ledger still holds the amount: reconcile by the original key first. A third test rejects reuse of the key with a different amount. These are local provider-contract examples, not real-bank guarantees.

The paired execution refactor uses the CSV's approved applicant. Before refactoring, a credit read is hidden in input evaluation and graph plus SDK each allow three attempts: nine underlying funding calls. Afterward, credit has its own node and graph owns a three-attempt budget: three calls. Both versions deliberately lose all funding responses, fail the funding node, and retain one committed effect under the same business key. This changes the operational budget explicitly; it does not pretend the external action never happened.

Exercise recovery using the fixed source's actual test suites:

bash chapters/34-labs/assets/coding-practices/run-runtime-seams.sh

The script checks the exact commit and rejects zero tests, skips, and failures in all four selected suites. Lease coordination revokes permission from an old claim, checkpoint writes reject its stale epoch, and dispatch tests retain a permit until actual worker exit. Session tests distinguish restoring old state from new progress. A separate workshop test creates three successive managers over the same stores: recovery counts remain 1, 2, and 3 with the old error retained, then a newly committed round resets the count to zero and clears the error. These fixtures use in-memory stores and controlled workers, not production databases.

ExperimentRequired observationPractices
Move approval ahead of blacklistThe high-score blacklisted applicant is incorrectly approved; a simulated action occursBP-06, BP-07, BP-14
Replace the frozen expected answer with the mutant's outputThe forged answer accepts the regression; the unchanged CSV answer rejects itBP-14
Replace first directly with uniqueTwo matching rules conflict; this is not equivalent rejectionBP-07
Add otherwise to collectBoth high-score and default items appearBP-07
Convert a policy code under Turkish localeThe application function still returns POLICY-IBP-02
Invoke EffectPort twice with one keyThe real method count is two; the declaration did not deduplicateBP-09

The counterexample tests demonstrate controlled mistakes by asserting their divergence from fixed answers. A passing counterexample demonstration does not approve its deliberately incorrect implementation.

Act five: explain the evidence boundary​

Expect 23 workshop tests: twelve standard examples, six decision/function/effect-key tests, four credit/funding tests, and one repeated-takeover test, with zero skipped. The separate runtime command reports its actual count by suite. These results cover teaching inputs and controls, not real-provider deduplication, every lending policy, or production capacity.

Transfer the policy to the verification Arc's Scenarios while retaining separate business-answer, path, and effect observations. The runtime command covers declared recovery contracts; in-memory tests do not establish multi-process database or production qualification.

Deliverables and stop rule: Supply the choice card, both graphs, fixed Oracle, full-output comparison, and one detected mutation. Fewer lines or nodes are insufficient. Stop expanding claims when the approved answer is absent or an unknown external result remains unresolved.

Recap​

  • The foundation capstone proves you can read, extend, and verify an order DAG against a graph contract.
  • The advanced capstone proves you can keep fallback, branching, and observation facts explainable.
  • The graduation capstone combines graph, tests, Scenario/Policy/Fixture, evidence interpretation, architecture diagrams, and unproven claims into a reviewable artifact.
  • Three optional extensions cover batch, await, and decision tables without changing the graduation standard.
  • Technical tests, business verification, and release approval are three ownership layers; passing one cannot substitute for the next.

Next Step​

You have completed Head First BLOGE. The final artifact is not “one more DSL file”; it is a body of work that another person can run, explain, and challenge. From here:


Coding Agent: Open the versioned task guide.