design-thinking
Generated at build time from
skills/design-thinking/SKILL.md. Do not edit this page directly.
Design Thinking turns a known engineering contract into an implementation graph.
X → Implementation Graph → Program<A, E, R>│ │ │ │ ││ │ │ │ └─ contextual capabilities (§5)│ │ │ └──── expected failures (§4)│ │ └─────── successful values/flow (§2)│ ││ └─ Effect<A, E, R> when Effect is used│└─ engineering contract or local implementation task§1 Shapes domain records, IDs, variants, errors§2 A successful transformation / call graph§3 Execution cardinality, ordering, concurrency, interruption§4 E / Cause expected failures, defects, interruption, recovery ownership§5 R contextual capabilities and provisioning§6 Boundaries decode external ingress; encode external egress§7 Policies retry, timeout, caching, tracing, limits, recovery§8 Scope resource and child-work lifetime§9 Verification smallest faithful proof§10 Effect project graph onto the installed Effect version when applicable§11 Code implementation paths should trace back to the graphRead the engineering contract or local task. Model only the implementation semantics that matter. Then write code whose consequential paths can be explained by that graph.
Do not redesign the product or system architecture here.
1. Name the shapes
Section titled “1. Name the shapes”Define the values that actually move through the implementation.
Typical shapes:
- Records — domain values with meaningful fields.
- IDs — identifiers when identity matters.
- Variants — explicit alternatives or states.
- Errors — expected failure values when callers can meaningfully handle them.
- Commands / events — when behavior is driven by requests or emitted facts.
Use stronger types when they remove real ambiguity or invalid states.
Do not require every ID to be branded, every primitive to become a wrapper, or every internal structure to become a named domain type.
Prefer the simplest representation that preserves the important invariant.
Example:
BookingRequestBookingRoomIdBookingStatus = Pending | Confirmed | CancelledBookingConflictThe shapes are the nouns. The graph is the behavior between them.
2. Think A first: successful flow
Section titled “2. Think A first: successful flow”Map the successful transformation before error handling.
Input ↓F1 ↓F2 ↓F3 ↓OutputFor each node ask:
What value comes in?What value comes out?What state changes?What external effect occurs?The happy path should remain easy to read even after failure and runtime policy are added.
Do not force every operation into a service or abstraction. A local function can stay a local function when that is where the behavior belongs.
If the system contract already defines responsibility boundaries, preserve them unless implementation evidence shows a real contradiction that must go back to engineering-design.
3. Model execution semantics separately
Section titled “3. Model execution semantics separately”Cardinality, execution ordering, concurrency, and freshness are different concerns.
Cardinality
Section titled “Cardinality”Ask whether the computation produces:
one resultorincremental / many results over timeA collection returned once is still a one-shot result:
Program<ReadonlyArray<User>, ...>It does not automatically require a stream.
Use streaming only when incremental consumption, continuous emission, backpressure, or long-lived flow is actually part of the behavior.
Ordering
Section titled “Ordering”Mark edges where order matters:
A↓B↓CDo not parallelize steps merely because they can technically run concurrently.
Example:
reserveStock ↓chargePaymentmay require ordering because the state transition semantics depend on it.
Concurrency
Section titled “Concurrency”When independent work may overlap, mark the intended execution explicitly:
fetchProfile ──┐ ├─ parallel → composeUserfetchSettings ─┘Possible execution forms include:
sequentialparallelbounded concurrencyracebackground / forkedFor concurrent work ask:
What may run at the same time?What ordering must still hold?What shared state exists?What happens if one branch fails?What happens if the parent is cancelled/interrupted?Freshness / caching
Section titled “Freshness / caching”Caching is a policy axis, not cardinality.
When caching matters define only the semantics required:
always recomputememoizedTTLexplicit invalidationdeduplicate concurrent lookupsDo not add caching because a result is “time-bounded” unless the product/system contract actually permits stale data and benefits from reuse.
4. Model failure correctly: E, defects, interruption
Section titled “4. Model failure correctly: E, defects, interruption”Do not treat retry, fallback, and die as equivalent kinds of failure.
Model outcomes as:
Node ↓Outcome ├─ Success A │ ├─ Expected failure E │ ├─ propagate │ ├─ map │ ├─ recover │ ├─ retry │ └─ fallback │ ├─ Defect │ └─ unexpected/programmer/runtime failure │ └─ Interruption └─ cancellation + finalization semanticsExpected failure
Section titled “Expected failure”Use E for failures that are part of the program’s expected contract and can be reasoned about by callers.
Examples:
BookingConflictNotFoundPermissionDeniedRateLimitedValidationErrorFor each expected failure decide who semantically owns the policy:
operation itselfcallerservice boundarytransport boundaryHandle it at the narrowest boundary that actually owns the recovery policy.
Example:
loadCache(id) └─ CacheMiss ↓ local recovery loadDatabase(id)This local recovery is clearer than forcing every error to an outermost handler.
Defect
Section titled “Defect”A defect is not just another E strategy.
Examples:
impossible invariantunexpected null from trusted codebug in implementationunrecoverable runtime assumptionNormally allow defects to propagate to the runtime/appropriate boundary unless there is a deliberate containment policy.
Do not convert every defect into a domain error merely to make the type look complete.
Interruption
Section titled “Interruption”When work can be cancelled/interrupted, reason about:
what may stopwhat must finishwhat must be cleaned upwhat state may already have changedwhat child work inherits cancellationInterruption semantics matter especially around concurrency and resources.
Error translation
Section titled “Error translation”Translate low-level failures where abstraction ownership changes.
Example:
SqlError ↓ repository boundaryDatabaseError ↓ service boundaryBookingFailure ↓ transport boundaryAPI errorDo not leak implementation-specific failures through boundaries that should hide them.
5. Model R as contextual capability, not every dependency
Section titled “5. Model R as contextual capability, not every dependency”R represents capabilities required from context to execute the computation.
Good candidates:
UserRepositoryClockEmailSenderConfigPaymentGatewayFileSystemNot every value belongs in R.
Use ordinary arguments for request/local data:
findUser(userId)userId is data, not a contextual service.
Distinguish:
request/local data→ function arguments
stable substitutable capability→ R / contextual service
dependency required only to construct a service→ satisfy during construction / LayerDo not leak service-construction dependencies into every consumer merely because the implementation happens to need them internally.
Use dependency injection only where substitution, lifecycle, configuration, or boundary ownership makes it useful.
Do not create one service interface per function.
6. Decode at every external trust transition
Section titled “6. Decode at every external trust transition”Every external ingress is a trust boundary.
Examples:
HTTP requestmessage queue payloadthird-party responseenvironment/configfile contentsdatabase data crossing a separately trusted boundaryCLI inputwebhookModel:
unknown / external ↓decode / parse / validate ↓domain representationInside that validated boundary, do not repeatedly parse the same value without a new trust transition.
But decoding proves only the constraints represented by the decoder/schema.
It does not automatically prove:
authorizationownershipfreshnessbusiness invariantscross-record consistencytransaction safetyThose belong to their actual semantic owners.
At external egress, encode to the external contract when needed:
domain value ↓encode / serialize ↓external representationReuse one schema only when the semantics are genuinely the same.
Do not force:
transport representationpersistence representationdomain representationto share one schema merely to avoid duplication.
When the repository uses Effect Schema, use the installed version’s decode/encode APIs.
7. Compose runtime policies as real semantics
Section titled “7. Compose runtime policies as real semantics”Runtime policies include:
retrytimeoutrecovery/fallbackcachingloggingtracingrate/concurrency limitsschedulingThese are not decoration.
They can change:
AERcontrol flownumber of executionstimingcancellation behaviorresource usageKeep policies visually separate from the core domain flow when that improves readability, but model their semantic effect.
Retry safety
Section titled “Retry safety”Never derive:
network timeout→ retryautomatically.
First ask:
Can the operation be repeated safely?Model:
retry candidate ↓repeat-safe / idempotent? ├─ yes → define retry policy └─ no → define one of: idempotency key deduplication status reconciliation uncertain-result handling no automatic retryExample:
chargePayment ↓ response timeoutBlind retry may double-charge unless the operation has a safe repeat strategy.
Timeouts
Section titled “Timeouts”Timeouts are semantic choices.
Ask:
What happens to the underlying work after the caller times out?Can it still commit?Can the caller retry?How is an uncertain result reconciled?Do not treat timeout as equivalent to “operation did not happen.”
Caching
Section titled “Caching”If caching changes freshness or consistency, make that visible in the graph.
Logging/tracing
Section titled “Logging/tracing”Instrumentation should observe the implementation without becoming the implementation’s domain control flow unless the contract actually depends on it.
8. Scope resources and child work
Section titled “8. Scope resources and child work”For resources such as:
database connectionsfile handlessocketsstreamschild processestemporary resourcessubscriptionsmodel:
acquire ↓owner scope ↓use ↓child work / fibers ↓release/finalizationThe implementation must make resource lifetime explicit enough that cleanup follows the intended owner lifecycle.
When Effect is used, scoped resource combinators and Scope should represent that lifetime according to the installed version.
Do not state resource cleanup as a generic “type guarantee.” The guarantee comes from using the runtime’s scoped acquisition/finalization semantics correctly.
For forked/background work ask:
Does it belong to the parent's scope?Should interruption cancel it?Can it outlive the request?Who owns cleanup?Unowned background work is a lifecycle bug waiting to happen.
9. Choose the smallest faithful verification
Section titled “9. Choose the smallest faithful verification”Verification is part of implementation, but not every test problem belongs here.
For obvious cases:
pure business rule→ targeted unit test
simple bug→ direct regression test
existing integration pattern clearly proves it→ use that patternUse test-engineering when testing itself requires design:
transaction semanticsconcurrency/racemigration compatibilityasync/queue semanticsfailure/recoverycomplex integration boundaryflaky/brittle suiteSubstitute R only when fidelity is preserved
Section titled “Substitute R only when fidelity is preserved”A fake dependency is useful when the property under test lives above that boundary.
Examples:
business decision→ fake repository may be sufficient
time-based retry policy→ controllable/test clock may be sufficientUse the real dependency when correctness belongs to that dependency’s semantics:
SQL uniqueness→ real DB
transaction isolation→ real DB + concurrency
filesystem semantics→ real filesystem boundary
HTTP serialization→ real integration boundary
migration behavior→ real migration against representative schema/dataDo not claim that swapping R proves the whole graph correct.
It proves only the behavior whose semantics are preserved by that substitution.
Core rule:
Choose the smallest testthat faithfully provesthe actual risk.10. Project to Effect only when Effect is actually used
Section titled “10. Project to Effect only when Effect is actually used”The graph model is language/framework-neutral.
Use Effect-specific projection only when:
the repository uses Effectorthe task explicitly requests Effect-style implementationBefore writing Effect API-specific code:
inspect package.json / lockfile ↓identify installed Effect version ↓use matching repository APIs/docs/patternsDo not write APIs from another Effect version.
A / E / R projection
Section titled “A / E / R projection”When applicable:
A→ success value/data flow
E→ expected typed failure
R→ contextual capabilities required to runDefects and interruption are not simply extra variants of E; reason about the runtime/Cause model appropriate to the installed version.
Effect.gen
Section titled “Effect.gen”Use Effect.gen when it makes sequential success flow clearer.
Do not impose:
gen body = all Aouter pipe = all Eas a correctness invariant.
Better rule:
Keep the happy path visually dominant.
Handle an expected failure at the narrowest boundarythat owns its recovery policy.Examples:
uniform recovery for the whole workflow→ outer composition may be appropriate
operation-specific fallback→ local handling around that operation
boundary error translation→ handle at the boundary
caller-owned failure→ propagate in E.pipe()
Section titled “.pipe()”.pipe() is composition syntax, not proof that the graph is unchanged.
Combinators inside a pipe may alter:
AERexecution counttimingconcurrencycancellationresource behaviorUse .pipe() where it improves composition/readability, while reasoning about the real semantics of each combinator.
Use Layer/provisioning to satisfy contextual capabilities and service-construction dependencies where appropriate.
Do not expose construction dependencies in service interfaces when they can be discharged during provisioning.
Stream
Section titled “Stream”Use Stream for incremental/multi-emission computations when its semantics are actually needed.
Do not convert every collection or paginated result into Stream by default.
11. Write code that traces to the graph
Section titled “11. Write code that traces to the graph”The graph is a model, not an absolute source of truth.
Every consequential implementation path should trace back to it.
Consequential means behavior such as:
data transformationstate transitiondependencyexpected failurerecovery policyconcurrency/orderingtrust boundaryresource lifetimeexternal effectIf code introduces consequential behavior that the graph cannot explain:
either the implementation is wrongor the graph is incompleteUpdate the correct side.
Do not force minor syntax, helper extraction, or incidental language detail into the graph.
The graph exists to make important semantics explicit, not to mirror every line of code.
Relationship to other skills
Section titled “Relationship to other skills”product-design
Section titled “product-design”Owns:
what product behavior is requiredDesign Thinking does not reopen product scope during implementation.
engineering-design
Section titled “engineering-design”Owns:
system responsibilitiesstate ownershipboundaries/contractssystem guaranteesarchitectural decisionsDesign Thinking consumes that contract and decides how the local software graph implements it.
If implementation reveals an architectural contradiction, return the issue to engineering-design.
design-graph
Section titled “design-graph”Use when implementation includes non-trivial interface interaction/state.
design-graph owns surface/move/void/attention modeling.
Design Thinking owns the software/runtime behavior behind that interface.
test-engineering
Section titled “test-engineering”Design Thinking owns obvious targeted verification.
test-engineering owns verification when the proof boundary or environment itself requires design.
security-review
Section titled “security-review”Design Thinking implements known security controls.
security-review determines whether changed trust/authority/data paths violate a security property.
code-review
Section titled “code-review”Design Thinking is forward reasoning:
intent / engineering contract→ implementation graph→ codeCode Review is reverse reasoning:
code / diff→ actual graph→ compare with intentDo not use Design Thinking as a substitute for post-implementation review.
production-ops
Section titled “production-ops”Production Ops owns runtime health, observability, incident response, and recovery requirements.
Design Thinking implements application-level changes required by those operational properties.
Proportionality
Section titled “Proportionality”Depth follows implementation risk.
tiny local change→ inspect → change → targeted verification
normal behavior change→ shapes + success flow + expected failure + dependencies
state / boundary / concurrency / resource change→ model the affected execution graph explicitly
complex/high-risk implementation→ use the full methodDo not force all sections onto every change.
Use only the parts that can change correctness or maintainability.
The Pipeline
Section titled “The Pipeline”ENGINEERING CONTRACT / LOCAL TASK ↓"What values exist?" → define only meaningful shapes
↓"What successful transformation must happen?" → draw A: implementation/call graph
↓"What ordering, cardinality, concurrency, or interruption matters?" → execution semantics
↓"What expected failures exist?""What defects/interruption matter?" → E / runtime failure model
↓"What contextual capabilities are required?" → R / provisioning
↓"Where does external data cross trust boundaries?" → decode ingress / encode egress
↓"What runtime policies change semantics?" → retry / timeout / cache / recovery / limits
↓"What resources or child work have lifetimes?" → scope / finalization
↓"What is the smallest faithful proof?" → targeted verification → test-engineering only if proof itself is non-trivial
↓"Does this repo actually use Effect?" ├─ no → preserve language-native implementation └─ yes → inspect installed version and project graph to Effect
↓CODEEvery consequential implementation path should trace to the graph. If code introduces behavior, dependency, failure, concurrency, boundary, or lifetime semantics that the graph cannot explain, either the implementation or the graph is incomplete.