Three Shapes
Three Shapes: the same six units of work arranged three ways - a chain, a star, and staged joins - then measured and critiqued. Design is choosing a shape ON PURPOSE, and purpose needs numbers.
Plans & Graphs
Round 5
Sandi Metz
exit 0
bundle exec ruby examples/three_shapes.rb
a real captured run
THREE SHAPES: six tasks x 40ms, concurrency 4 shape wall depth max fan-in the trade chain 246ms 6 1 trivially debuggable; pays full serial price star 121ms 3 4 fastest; one join owns every failure mode staged 125ms 3 3 nearly as fast; each join has one reason to wait none of these is wrong. the chain is right when the work is truly sequential; the star when the join is trivial; the staged shape when the join has judgment in it. what's wrong is not knowing which one you built - and now the graph will tell you, in two numbers.
source
# frozen_string_literal: true # Three Shapes: the same six units of work arranged three ways - a # chain, a star, and staged joins - then measured and critiqued. Design # is choosing a shape ON PURPOSE, and purpose needs numbers. # # bundle exec ruby examples/three_shapes.rb # # Runs offline; each unit of work is 40ms of simulated IO. require class="s">"bundler/setup" require class="s">"agentic" UNIT = 0.04 NAMES = %w[gather_a gather_b gather_c gather_d combine finish].freeze def build(shape) orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4) tasks = NAMES.to_h do |name| [name, Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"work"})] end deps = case shape when class="y">:chain # one long line: simple to read, nothing overlaps NAMES.each_cons(2).to_h { |a, b| [b, [a]] } when class="y">:star # everything at once, one god join {class="s">"combine" => %w[gather_a gather_b gather_c gather_d], class="s">"finish" => [class="s">"combine"]} when class="y">:staged # balanced: pairs join, then join the joins {class="s">"gather_c" => [], class="s">"gather_d" => [], class="s">"combine" => %w[gather_a gather_b], class="s">"finish" => %w[combine gather_c gather_d]} end NAMES.each do |name| orchestrator.add_task(tasks[name], (deps[name] || []).map { |d| tasks.fetch(d) }, agent: ->(_t) { sleep(UNIT) || class="y">:ok }) end orchestrator end # Structural facts, read straight off the graph def shape_facts(graph) dependencies = graph[class="y">:dependencies] depth = {} measure = ->(id) { depth[id] ||= 1 + (dependencies[id].map { |d| measure.call(d) }.max || 0) } { max_fan_in: dependencies.values.map(&class="y">:size).max, depth: dependencies.keys.map { |id| measure.call(id) }.max } end puts class="s">"THREE SHAPES: six tasks x #{(UNIT * 1000).round}ms, concurrency 4" puts puts format(class="s">" %-8s %-10s %-8s %-9s %s", class="s">"shape", class="s">"wall", class="s">"depth", class="s">"max fan-in", class="s">"the trade") TRADES = { chain: class="s">"trivially debuggable; pays full serial price", star: class="s">"fastest; one join owns every failure mode", staged: class="s">"nearly as fast; each join has one reason to wait" }.freeze %i[chain star staged].each do |shape| orchestrator = build(shape) facts = shape_facts(orchestrator.graph) result = orchestrator.execute_plan puts format(class="s">" %-8s %4dms %-8d %-9d %s", shape, result.execution_time * 1000, facts[class="y">:depth], facts[class="y">:max_fan_in], TRADES[shape]) end puts puts class="s">"none of these is wrong. the chain is right when the work is truly" puts class="s">"sequential; the star when the join is trivial; the staged shape when" puts class="s">"the join has judgment in it. what's wrong is not knowing which one" puts class="s">"you built - and now the graph will tell you, in two numbers."