Overview
This section provides a code-first approach to learning software architecture through heavily annotated examples in four functional languages: F# (canonical), Clojure, TypeScript, and Haskell. Each example presents all four languages as parallel tabs — F# carries the deepest annotations and the framing prose, while Clojure, TypeScript, and Haskell are first-class variants showing the same architectural decision in their respective idioms. Each example also mirrors its counterpart in the OOP variant (same number, same conceptual title) so the two paradigms can be compared side-by-side.
What You Will Learn
The examples in this section cover patterns, principles, architectural styles, trade-offs, and real-world architectural decisions across three progressive levels, expressed in idiomatic F#, Clojure, TypeScript, and Haskell:
- Beginner: Foundational architectural concepts with simple, self-contained examples in all four languages
- Intermediate: Composite patterns and common enterprise architecture challenges
- Advanced: Complex systems, distributed architecture, and nuanced trade-off analysis
How to Use This Section
Each example is self-contained and annotated to explain not just what the code does, but why each architectural decision was made. The F# tab runs under dotnet fsi; the Clojure tab runs as a standalone namespace; the TypeScript tab runs under ts-node or deno; the Haskell tab runs under runghc or cabal run. Start at the level that matches your current understanding and progress through the examples in order. Each FP example shares the same number as its OOP counterpart in the in-oop-by-example sibling tutorial, enabling cross-paradigm comparison.
Paradigm-Fit Legend
Not every architectural pattern fits every paradigm equally. Many examples carry a Paradigm Note banner explaining whether the pattern is FP-native, OOP-native, or paradigm-neutral. The classification follows authoritative sources:
- NEUTRAL — paradigm-agnostic concept (microservices, distributed tracing, hexagonal architecture). Both tracks teach legitimately.
- OOP-NATIVE — pattern emerged from OOP and is absorbed by FP language features. Norvig (1996, Design Patterns in Dynamic Languages) classified 16 of 23 GoF patterns this way. Examples in the FP track show the native FP idiom (HOF, ADT, fold, FRP) rather than reproducing the OOP shape.
- OOP-NATIVE-BUT-TRANSFERABLE — OOP roots but the concept transfers cleanly (SOLID, DDD aggregates, Repository). The paradigm note explains the FP encoding.
- FP-NATIVE — pattern emerged from or expresses most naturally in FP (Railway-Oriented Programming, Free Monads, Reader/State monads, Event Sourcing fold, FRP, Kleisli composition). Examples 86–90 are FP-native extras; the OOP track carries stubs pointing here.
Authority basis: Norvig 1996; Seemann (Design patterns across paradigms, 2012; SOLID: the next step is Functional, 2014); Wlaschin (Domain Modeling Made Functional); Hickey (Simple Made Easy); Evans (Domain-Driven Design); Fowler (PEAA).
Rust as an FP-Adjacent Member — With Concept Adjustments
Rust shares deep DNA with the FP languages on this track: enum is a sum type (the Rust Reference explicitly calls it "analogous to a data constructor declaration in Haskell" — Blandy & Orendorff, Programming Rust, Ch. 10), match is exhaustive pattern matching, traits cover much of the territory typeclasses cover, and Result<T, E> is the Railway-Oriented Programming primitive built into stdlib. But the FP concepts taught in this track must be adjusted before they translate to Rust — Rust is not "F# with a borrow checker." Patterns that translate one-to-one (sum types, Result, pure functions, exhaustive match, smart constructors) appear without adjustment. Patterns where ownership is the design force live in the in-procedural-by-example track.
Six concept adjustments when you read this track through a Rust lens:
- Ownership / affine types — no F# equivalent. Every Rust value is used at most once before it is moved. Passing a captured variable to
mapmay invalidate the closure that captured it. The closures, folds, and pipelines this track teaches must account for moves; F# / Haskell GC-managed sharing has no analogue. (Source: without.boats — Ownership) - No higher-kinded types (HKT). Rust cannot abstract over
Optionwithout a type argument — only overOption<T>. Therefore Rust has no genericFunctororMonadtrait. Each effect type carries its ownmap,and_then, etc. Examples 86–90 (Free Monads, Reader, Kleisli, State) express patterns that require HKT in their canonical form; in Rust they collapse to instance-specific combinators or are simulated via Generic Associated Types (GATs, stabilised Rust 1.65) which approach but do not reach HKT. (Source: GAT stabilisation post — Niko Matsakis & Jack Huey — explicitly: "not full-blown higher-kinded polymorphism") ?operator is sugar, not monadic bind. F#Result.bindis a generic monadic operation; Rust's?expands to a specificmatchthat returns early onErr(e)afterFrom::from(e). The teaching framing "chain fallible steps withbind" becomes "short-circuit onErrwith?" — same engineering effect, different conceptual machinery. (Source: Rust By Example —?operator)async/Futureis not a monad. F#'sasync { }is a monadic computation expression. Rust'sasync fndesugars to aFuturestate machine sequenced by the compiler, not by a monadic bind. There is no equivalent toasyncResult { }as a generic abstraction —tokio-ecosystem libraries provide ad-hoc combinators instead.- No persistent immutable shared structures by default. F# lists / maps / sets share structure via GC. Rust's defaults are owned, not shared. Persistent structures exist in the
imcrate (or viaArc<T>), but the default style is "move owned data" rather than "share immutable references." - Traits ≈ typeclasses minus HKT. Single-type-parameter abstractions (
Display,Iterator,Clone) map cleanly. Multi-type-parameter or kind-polymorphic abstractions (Functor, Monad, Free) do not. Read teaching about "typeclass-style abstraction" with this caveat.
Where the FP idiom translates one-to-one, Rust appears as an additional language tab alongside F# / Clojure / TypeScript / Haskell. Where the idiom requires an ownership-driven re-formulation (typestate, RAII, borrowed slices), the example points to the procedural track instead.
Structure of Each Example
Every example follows a consistent five-part format:
- Brief Explanation — what the pattern or principle addresses and why it matters (2-3 sentences)
- Mermaid Diagram — visual representation of component relationships, pipelines, or data flow (when appropriate)
- Heavily Annotated Code — parallel tabs showing F# (canonical), Clojure, TypeScript, and Haskell, each with
// =>(or;;in Clojure,-- =>in Haskell) comments documenting architectural decisions and trade-offs - Key Takeaway — the core insight to retain from the example (1-2 sentences)
- Why It Matters — production relevance and real-world impact (50-100 words)
Examples by Level
Beginner (Examples 1–28)
- Example 1: No Separation vs. Clear Separation
- Example 2: Single Responsibility Principle
- Example 3: Three-Layer Architecture
- Example 4: Presentation Layer Isolation
- Example 5: Model-View-Controller Basics
- Example 6: Model Encapsulates Validation
- Example 7: Manual Dependency Injection
- Example 8: Constructor Injection vs. Method Injection
- Example 9: Interface Segregation Principle
- Example 10: Open for Extension, Closed for Modification
- Example 11: Subtypes Must Be Substitutable
- Example 12: DRY — Don't Repeat Yourself
- Example 13: KISS — Keep It Simple, Stupid
- Example 14: YAGNI — You Aren't Gonna Need It
- Example 15: High Coupling — The Problem
- Example 16: Low Coupling Through Encapsulation
- Example 17: Cohesion — Grouping Related Behavior
- Example 18: Encapsulation with Private State
- Example 19: Preferring Composition
- Example 20: Mixin vs. Composition
- Example 21: Repository Pattern Basics
- Example 22: Repository with Query Methods
- Example 23: Service Layer Coordinates Use Cases
- Example 24: Service Layer with Error Handling
- Example 25: Data Transfer Objects
- Example 26: DTO Validation
- Example 27: Small Layered Application
- Example 28: Recognizing Architecture Smells
Intermediate (Examples 29–57)
- Example 29: Hexagonal Architecture — Ports and Adapters
- Example 30: Clean Architecture — Layer Separation with Dependency Rule
- Example 31: Onion Architecture — Domain at the Center
- Example 32: Observer Pattern — Event Notification Without Coupling
- Example 33: Domain Events — Signaling State Changes Within a Bounded Context
- Example 34: Event-Driven Architecture — Async Message Passing Between Services
- Example 35: Strategy Pattern — Swappable Algorithms
- Example 36: Factory Pattern — Centralized Object Creation
- Example 37: Builder Pattern — Constructing Complex Objects Step by Step
- Example 38: Adapter Pattern — Bridging Incompatible Interfaces
- Example 39: Decorator Pattern — Adding Behavior Without Subclassing
- Example 40: Facade Pattern — Simplified Interface to a Subsystem
- Example 41: Command Pattern — Encapsulate Actions as Objects
- Example 42: Mediator Pattern — Centralized Component Coordination
- Example 43: State Pattern — Objects That Change Behavior Based on State
- Example 44: HOF with Hole-Filling Steps
- Example 45: Value Objects — Immutable Domain Concepts Without Identity
- Example 46: Aggregate Roots — Consistency Boundaries in DDD
- Example 47: Bounded Contexts — Separating Domain Models by Responsibility
- Example 48: Anti-Corruption Layer — Protecting the Domain from External Models
- Example 49: CQRS Pattern — Separate Read and Write Models
- Example 50: Middleware Pattern — Processing Pipeline for Cross-Cutting Concerns
- Example 51: Plugin Architecture — Extending Systems Without Modifying Core
- Example 52: Repository Pattern — Abstracting Data Access
- Example 53: Unit of Work Pattern — Grouping Operations into Atomic Transactions
- Example 54: Specification Pattern — Composable Business Rules
- Example 55: CQRS with Event Sourcing — State as a Sequence of Events
- Example 56: Saga Pattern — Managing Distributed Transactions
- Example 57: Circuit Breaker Pattern — Preventing Cascade Failures
Advanced (Examples 58–85)
- Example 58: Microservices Decomposition by Business Capability
- Example 59: Strangler Fig Pattern
- Example 60: Saga Orchestration
- Example 61: Saga Choreography
- Example 62: API Versioning Strategies
- Example 63: Backend for Frontend (BFF) Pattern
- Example 64: Circuit Breaker with Fallback
- Example 65: Bulkhead Pattern
- Example 66: Retry with Exponential Backoff and Jitter
- Example 67: Distributed Tracing Architecture
- Example 68: Sidecar Pattern
- Example 69: Ambassador Pattern
- Example 70: Event Sourcing Implementation
- Example 71: Modular Monolith
- Example 72: Vertical Slice Architecture
- Example 73: Shared Kernel
- Example 74: Specification Pattern
- Example 75: Chain of Responsibility
- Example 76: Visitor Pattern in Architecture
- Example 77: Database per Service Pattern
- Example 78: Feature Toggle Architecture
- Example 79: Service Mesh Architecture
- Example 80: Interpreter Pattern for Configuration DSL
- Example 81: CQRS (Command Query Responsibility Segregation)
- Example 82: Outbox Pattern for Reliable Event Publishing
- Example 83: Anti-Corruption Layer
- Example 84: Ports and Adapters (Hexagonal Architecture)
- Example 85: Reactive Architecture with Backpressure
FP-Native Extras (Examples 86–90)
Patterns that have no natural OOP counterpart — they exist in FP because the paradigm makes them ergonomic.
- Example 86: Railway-Oriented Programming (Result/Either Chains)
- Example 87: Free Monads / Tagless Final (Embedded DSLs)
- Example 88: Reader Monad for Dependency Injection
- Example 89: Kleisli Composition for Effectful Pipelines
- Example 90: State Monad for Pure Stateful Computation
OOP-Native Stubs (Examples 91–93)
Numbering parity with the OOP track; full treatment lives there.
Last updated May 16, 2026