Skip to main content

Online edition for BLOGE 0.9.8-RC1 · facts verified 2026-09-15 · 中文

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 1Ch 4)
  • Branches and resilience (Ch 5Ch 6)
  • Operator design and DSL authoring (Ch 7Ch 8)
  • Subgraphs, iteration, waiting, and durability (Ch 10Ch 13)
  • Orchestration patterns — sessions, state machines, and nested lifecycle ownership (Ch 14Ch 16)
  • Tooling and review feedback (Ch 9)
  • Testing, production wiring, and scale (Ch 18Ch 22)
  • Business correctness, evidence, and release claims (Ch 23Ch 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:
    • truenotifySuccess (a new NotifySuccessOperator node)
    • falsenotifyPartialFailure (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
    • otherwisehandleTimeout (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.

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.