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
- Evolve a graph from a bounded starter instead of guessing syntax in an empty file.
- Deliver separate evidence for the graph contract, business contract, and release boundary.
- Run one-variable failure experiments for effects, waiting, recovery, and fallback.
- Explain the runtime boundary in one architecture diagram and the evidence boundary in one receipt-reading diagram.
- 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
| File | What it shows |
|---|---|
order-process.bloge | Foundation start — fan-out, fan-in, branch, resilience |
ticket-routing.bloge | Advanced start — sentiment analysis and multi-way branch |
loan-approval.bloge | Graduation start — loan business and verification assets |
batch-order-parallel.bloge | Optional batch exercise — parallel foreach |
status-polling.bloge | Optional loop exercise — until and loopIteration |
payment-wait.bloge | Optional await reference — correlation and timeout |
order-saga.bloge | Graduation reference — saga compensation |
GraphTestRunner.java | Test harness used in every lab's verify step |
MockOperator.java | Test doubles — returning, throwing, delaying, recording |
DslTestHelper.java | DSL compile + expectParseError / expectCompileError |
missing-timeout.bloge | Antipattern reference — nodes without a timeout budget |
over-broad-fallback.bloge | Antipattern 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
foreachand 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:
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.
| Level | Expected effort | Starting file | Work contract | Completion standard |
|---|---|---|---|---|
| Foundation | 60–90 minutes | order-process.bloge | One DSL copy + graph tests; explicit steps | Success and rejection paths are reviewable |
| Advanced | 2–3 hours | ticket-routing.bloge | DSL, mocks, listener test; boundary hints only | Fallback and observation facts stay distinct |
| Graduation | 1–2 days | Loan starter + verification assets | Graph, tests, Scenario/Policy/Fixture, diagrams, report; acceptance contract only | Runnable, 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:
| Phase | Skill exercised | Chapter callback |
|---|---|---|
| Read the fixture | Dependency tracing, parallel identification | Ch 3 |
| Add a node | Operator binding, input wiring, depends_on | Ch 2, Ch 4 |
| Add resilience | retry, timeout, fallback, compensate | Ch 6 |
| Add a branch | branch on, otherwise, skipped-node reasoning | Ch 5 |
| Add iteration | foreach, loop, carry, until | Ch 11 |
| Add suspension | await, event correlation, on_timeout | Ch 12 |
| Write tests | GraphTestRunner, MockOperator, DslTestHelper | Ch 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 →
retrywithtimeoutper attempt. - Only add
fallbackwhen 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:
-
In
ticket-routing.bloge, which nodes run in parallel? (Answer:fetchCustomerandfetchTicketHistory— neither depends on the other.) -
In
loan-approval.bloge, what is the maximum parallelism afterfetchApplicationcompletes? (Answer: four —checkCredit,detectFraud,verifyIncome, andcheckBlacklistall depend only onfetchApplication.) -
In
batch-order-parallel.bloge, cansummarizestart before every foreach iteration has finished? (Answer: no —summarizedepends onprocessOrders, which completes only when all iterations are done.) -
What
MockOperatorfactory would you use to simulate a node that takes 500 ms and then succeeds? (Answer:MockOperator.delaying(Duration.ofMillis(500), output).) -
What
GraphTestRunnermethod 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:
- Add a
fetchInventorynode that runs in parallel withfetchUserandfetchProducts. Input:ctx.productIds. Timeout:2s. - Wire
fetchInventory.outputintocalcPriceas a newinventoryinput. Updatedepends_onaccordingly. - Add a
compensateblock tocreateOrderthat callsReleaseInventoryOperator— modelled after the pattern inorder-saga.bloge. - Verify: Write a test that asserts
fetchInventory,fetchUser, andfetchProductsall complete; thatcreateOrderexecutes when credit is approved; and thatrejectOrderis 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:
analyzeSentimentalready has a fallback. Addretry = { attempts: 2, backoff: 300ms, strategy: jitter }tofetchTicketHistoryso it can survive transient history-service failures.- Add an
escalateToManagernode as a fourth branch case forclassifyPriority.output.priority == "critical". Input:customerIdfromfetchCustomer.output.idandsentimentfromanalyzeSentiment.output. - Verify: Write one test where
classifyPriorityreturns{ priority: "critical" }and assert thatescalateToManagerexecutes whileassignVipAgent,assignNormalAgent, andautoResolveare 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:
- Add a
reserveFundsnode betweenaggregateRiskandmakeDecision.reserveFundsdepends onaggregateRiskand takesapplication = fetchApplication.outputandrisk = aggregateRisk.outputas input. - Give
reserveFundsanoutput { reservationId: String }declaration and acompensateblock callingReleaseFundsOperator— the same saga pattern used inorder-saga.bloge. - Update
makeDecisionso it alsodepends_onreserveFunds. - Add
timeout = 3storeserveFundsandfallback = { reservationId: "NONE", status: "degraded" }so a slow reserve doesn't block the pipeline. - Technical verification: Write a test with a
MockOperator.throwing(...)forReserveFundsOperatorthat proves the fallback fires and the graph still reachesmakeDecision. - Business verification: Cover at least
auto-approve,manual-review, andreject; forbid real effects in Policy and fix credit, fraud, and income facts in Fixture. - Evidence interpretation: Write separate conclusions for verdict, evidence trust/source binding, Requirement contribution, and release claim.
- Architecture: Submit one graph/runtime/effect boundary diagram and one evidence-to-claim diagram, naming the business owner and release owner.
- Unproven claims: Include production capacity, real downstream health, GOLDEN approval, and release authorisation.
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:
- Add
retry = { attempts: 1, backoff: 100ms, strategy: exponential }andtimeout = 3sto thedeductStocknode inside theforeachbody. - Add a
fallback = { deducted: false, reason: "stock service unavailable" }todeductStockso a single stock failure doesn't break the whole batch. - After
summarize, add abranch on summarize.output.allSucceeded:true→notifySuccess(a newNotifySuccessOperatornode)false→notifyPartialFailure(a newNotifyPartialFailureOperatornode)
- Verify: Wire
deductStockasMockOperator.throwing(...)for one item to confirm the fallback fires, then assertnotifyPartialFailureexecutes andnotifySuccessis 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:
deductStockis invoked exactly 20 times (each item attempted).summarize.output.failedCount == 3andallSucceeded == false.notifyPartialFailureruns once;notifySuccessis skipped.- With
batch_size = 10, the test observes at most 10 concurrentdeductStockinvocations (wrapMockOperator.of(...)with anAtomicInteger activecounter and update a secondpeakcounter 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:
- Add a
notifyReadynode afterfetchResultthat sends a notification when the result is available. Input:result = fetchResult.output. - Add
timeout = 5stocheckStatusinside the loop body. - Verify: Assert
notifyReadyexecutes afterfetchResult.
Part B — Replace the loop with await:
- Rewrite the graph to use
awaitinstead of a polling loop. AftersubmitJob, declare:await awaitJobReady {event "job.ready" where jobId = submitJob.output.jobIdtimeout = 60son_timeout {status = "timeout"reason = "Job did not become ready within 60 seconds"}} - Add a
branch on awaitJobReady.output.status:"ready"→fetchResultotherwise→handleTimeout(a new node that logs the failure)
- Verify: Write two tests — one that simulates the event arriving
(assert
fetchResultexecutes,handleTimeoutskipped) and one that simulates a timeout (asserthandleTimeoutexecutes,fetchResultskipped).
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:
- Remove
otherwisefrom thehit=uniquetable, then passscore=500. Assert that the graph throwsDecisionTableViolationExceptionwith codeRUNTIME_DECISION_TABLE_NO_MATCH. - Add an overlapping rule
score >= 700alongside the existingscore >= 750rule (both resolve forscore=800). Assert that the engine throwsRUNTIME_DECISION_TABLE_AMBIGUOUS_MATCHwithhit=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 forscore=780, amount=80000).- Both error-path tests assert the correct
codestring. bloge-lintreports zero WARNINGs afterotherwiseis 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:
- Ship a real graph — pick one workflow in your system, write the DSL,
wire the operators, and test it with
GraphTestRunner. - Explore advanced features — try wiring
DurableSessionManagerandDurableStateMachineManagerinto your test harness to practice crash-safe orchestration patterns. If your use case involves LLM tool-use, explore theagentDSL inbloge-agent-ext(covered in Chapter 17). - Consult the reference layer when you need exact syntax or edge-case behaviour:
- Explore advanced examples in
bloge-examples/— especiallyfood-order.bloge,claim-processing.bloge, andshipment-planning.blogefor graphs that combine everything you have learned. - Set up tooling — install the VS Code extension or IntelliJ plugin for real-time diagnostics as you author.
Reference Links
- Project README — module overview and quick start
- Getting Started — full setup guide
- DSL Specification — formal grammar reference
- Operator Design Specification — operator contract and lifecycle
- Core Architecture — engine internals
GraphTestRunner.java— test harness sourceMockOperator.java— test double sourceDslTestHelper.java— DSL compile helper sourceorder-process.bloge— foundation startticket-routing.bloge— advanced startloan-approval.bloge— graduation startbatch-order-parallel.bloge— optional batch exercisestatus-polling.bloge— optional loop exercisepayment-wait.bloge— optional await referenceorder-saga.bloge— saga compensation reference
Coding Agent: Open the versioned task guide.