Skip to content
AyoKoding

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:

  1. Ownership / affine types — no F# equivalent. Every Rust value is used at most once before it is moved. Passing a captured variable to map may 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)
  2. No higher-kinded types (HKT). Rust cannot abstract over Option without a type argument — only over Option<T>. Therefore Rust has no generic Functor or Monad trait. Each effect type carries its own map, 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")
  3. ? operator is sugar, not monadic bind. F# Result.bind is a generic monadic operation; Rust's ? expands to a specific match that returns early on Err(e) after From::from(e). The teaching framing "chain fallible steps with bind" becomes "short-circuit on Err with ?" — same engineering effect, different conceptual machinery. (Source: Rust By Example — ? operator)
  4. async / Future is not a monad. F#'s async { } is a monadic computation expression. Rust's async fn desugars to a Future state machine sequenced by the compiler, not by a monadic bind. There is no equivalent to asyncResult { } as a generic abstraction — tokio-ecosystem libraries provide ad-hoc combinators instead.
  5. 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 im crate (or via Arc<T>), but the default style is "move owned data" rather than "share immutable references."
  6. 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:

  1. Brief Explanation — what the pattern or principle addresses and why it matters (2-3 sentences)
  2. Mermaid Diagram — visual representation of component relationships, pipelines, or data flow (when appropriate)
  3. 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
  4. Key Takeaway — the core insight to retain from the example (1-2 sentences)
  5. Why It Matters — production relevance and real-world impact (50-100 words)

Examples by Level

Beginner (Examples 1–28)

Intermediate (Examples 29–57)

Advanced (Examples 58–85)

FP-Native Extras (Examples 86–90)

Patterns that have no natural OOP counterpart — they exist in FP because the paradigm makes them ergonomic.

OOP-Native Stubs (Examples 91–93)

Numbering parity with the OOP track; full treatment lives there.

Last updated May 16, 2026

Command Palette

Search for a command to run...