You are viewing a preview of this lesson. Sign in to start learning
Back to Functional Programming with F#

Functional Architecture

Design production systems with functional principles: pure core/imperative shell, concurrency, .NET interop, performance, and testing.

Functional Architecture

Master functional architecture with free flashcards and spaced repetition practice. This lesson covers designing systems with pure functions, composable modules, and immutable stateโ€”essential concepts for building maintainable F# applications. You'll learn to structure codebases that are testable, scalable, and resilient to change.

Welcome ๐Ÿ—๏ธ

Welcome to Functional Architecture! Architecture in functional programming differs fundamentally from object-oriented design. Instead of organizing code around mutable objects and classes, functional architecture emphasizes composability, immutability, and separation of pure logic from effects.

In this lesson, you'll discover how to structure F# applications using functional principles. We'll explore the onion architecture pattern adapted for functional programming, learn how to separate business logic from infrastructure concerns, and understand techniques for managing side effects at the boundaries of your system.

Core Concepts ๐Ÿ’ก

The Functional Core, Imperative Shell Pattern

The most fundamental principle of functional architecture is keeping pure functions at the core while pushing side effects to the edges. This pattern creates a clear boundary between:

Functional Core ๐Ÿงฎ

  • Pure functions with no side effects
  • Business logic and domain rules
  • Easily testable without mocks
  • Deterministic and cacheable

Imperative Shell ๐Ÿš

  • I/O operations (database, file system, network)
  • Framework integration
  • Thin layer that coordinates the core
  • Handles all "dirty" work
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚     IMPERATIVE SHELL (Thin)             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚                               โ”‚      โ”‚
โ”‚  โ”‚   FUNCTIONAL CORE (Thick)     โ”‚      โ”‚
โ”‚  โ”‚                               โ”‚      โ”‚
โ”‚  โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚      โ”‚
โ”‚  โ”‚   โ”‚  Pure Business      โ”‚    โ”‚      โ”‚
โ”‚  โ”‚   โ”‚  Logic & Domain     โ”‚    โ”‚ โ† DB โ”‚
โ”‚  โ”‚   โ”‚  Rules              โ”‚    โ”‚      โ”‚
โ”‚  โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚      โ”‚
โ”‚  โ”‚                               โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚                                          โ”‚
โ”‚  ๐ŸŒ HTTP  ๐Ÿ“ Files  ๐Ÿ“ง Email            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ’ก Tip: Aim for 80% pure code, 20% effects. The more logic you can push into pure functions, the easier testing and reasoning become!

Dependency Management

Traditional OOP: Inject dependencies through constructors or properties

Functional Approach: Pass dependencies as function parameters

// โŒ OOP style - requires mocking
type UserService(repository: IUserRepository) =
    member this.GetUser(id) = repository.FindById(id)

// โœ… Functional style - pure and testable
let getUser (findById: int -> User option) (id: int) =
    findById id
    |> Option.map processUser

In functional architecture, we often create function records to group related dependencies:

type DatabaseOperations = {
    findUser: int -> Async<User option>
    saveUser: User -> Async<unit>
    deleteUser: int -> Async<bool>
}

The Onion Architecture (Functional Style) ๐Ÿง…

The onion architecture arranges code in concentric layers, with dependencies pointing inward:

          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚   Infrastructure        โ”‚
          โ”‚  (DB, HTTP, Files)      โ”‚
          โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
          โ”‚  โ”‚  Application     โ”‚   โ”‚
          โ”‚  โ”‚  (Use Cases)     โ”‚   โ”‚
          โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
          โ”‚  โ”‚  โ”‚  Domain   โ”‚   โ”‚   โ”‚
          โ”‚  โ”‚  โ”‚  (Pure)   โ”‚   โ”‚   โ”‚
          โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
          โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

    Inner layers NEVER depend on outer layers

Domain Layer (Center):

  • Pure types and business rules
  • No dependencies on external libraries
  • Example: type Order = { Id: int; Items: OrderItem list; Total: decimal }

Application Layer:

  • Use cases and workflows
  • Composes domain functions
  • Defines interfaces for external dependencies
  • Example: Order processing pipeline

Infrastructure Layer (Outer):

  • Implementations of database access, HTTP clients, etc.
  • Framework-specific code
  • Instantiates and wires up dependencies

Railway-Oriented Programming ๐Ÿš‚

A key pattern for handling errors in functional pipelines:

type Result<'T, 'E> =
    | Ok of 'T
    | Error of 'E

let bind f result =
    match result with
    | Ok value -> f value
    | Error e -> Error e

let (>>=) = bind
RAILWAY ORIENTED PROGRAMMING

Happy Path: โ”€โ”€โ†’ Validate โ”€โ”€โ†’ Process โ”€โ”€โ†’ Save โ”€โ”€โ†’ โœ“

Error Path:     โ†“               โ†“          โ†“
                โ””โ”€โ”€โ†’ Error โ”€โ”€โ”€โ”€โ”€โ”€โ†’ Error โ”€โ”€โ†’ โœ—

This allows composing functions that might fail:

let processOrder orderId =
    loadOrder orderId
    >>= validateOrder
    >>= calculateTotal
    >>= applyDiscount
    >>= saveOrder

Ports and Adapters (Hexagonal Architecture) ๐Ÿ”ท

Also called Hexagonal Architecture, this pattern isolates core logic from external concerns:

Ports: Interfaces (function signatures) that define what the core needs Adapters: Concrete implementations that fulfill ports

// Port (interface in core)
type IEmailSender =
    abstract Send: recipient:string -> subject:string -> body:string -> Async<Result<unit, string>>

// Adapter (implementation in infrastructure)
type SmtpEmailSender() =
    interface IEmailSender with
        member _.Send recipient subject body =
            async {
                // SMTP implementation
                return Ok ()
            }

In F#, we prefer function types over interfaces:

// Port as function type
type SendEmail = string -> string -> string -> Async<Result<unit, string>>

// Adapter as function
let smtpSendEmail: SendEmail =
    fun recipient subject body ->
        async {
            // Implementation
            return Ok ()
        }

Organizing Modules by Feature ๐Ÿ“

Instead of organizing by technical layer (Models/, Controllers/, Services/), organize by feature or bounded context:

OrderManagement/
โ”œโ”€โ”€ Domain.fs          // Pure types and rules
โ”œโ”€โ”€ Operations.fs      // Pure business logic
โ”œโ”€โ”€ Validation.fs      // Pure validation
โ”œโ”€โ”€ Workflows.fs       // Use case orchestration
โ””โ”€โ”€ Infrastructure.fs  // DB, HTTP, etc.

UserManagement/
โ”œโ”€โ”€ Domain.fs
โ”œโ”€โ”€ Operations.fs
โ””โ”€โ”€ Infrastructure.fs

This approach:

  • โœ… Makes features self-contained
  • โœ… Reduces coupling between features
  • โœ… Simplifies navigation
  • โœ… Enables parallel team work

๐Ÿง  Memory Device: Think FOPIS - Feature Organization Promotes Isolation and Scalability!

Handling State ๐Ÿ”„

Functional architecture avoids mutable state. Instead:

1. Return new values (immutability):

let updateUserEmail user newEmail =
    { user with Email = newEmail }

2. Thread state through functions:

let processSteps initialState =
    initialState
    |> step1
    |> step2
    |> step3

3. Use agents for isolated mutable state:

let cacheAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop cache = async {
        let! msg = inbox.Receive()
        match msg with
        | Get (key, reply) ->
            reply.Reply(Map.tryFind key cache)
            return! loop cache
        | Set (key, value) ->
            return! loop (Map.add key value cache)
    }
    loop Map.empty)

Testing Functional Architecture ๐Ÿงช

Functional architecture makes testing dramatically simpler:

Pure functions: Test with simple assertions, no mocks needed

[<Fact>]
let ``calculateDiscount applies 10% for orders over 100`` () =
    let order = { Total = 150M; Items = [] }
    let result = calculateDiscount order
    Assert.Equal(135M, result.Total)

Functions with dependencies: Pass test doubles

[<Fact>]
let ``processOrder saves valid order`` () =
    let mutable saved = None
    let mockSave order = async { saved <- Some order; return Ok () }
    
    processOrder mockSave validOrder |> Async.RunSynchronously
    
    Assert.True(saved.IsSome)

๐Ÿค” Did you know? Some functional codebases achieve 100% test coverage of business logic because all the core is pure functions that are trivial to test!

Examples with Explanations

Example 1: E-Commerce Order Processing Pipeline

Let's build a complete order processing system using functional architecture:

// Domain.fs - Pure types, no dependencies
module OrderManagement.Domain

type OrderItem = {
    ProductId: int
    Quantity: int
    Price: decimal
}

type Order = {
    Id: int
    CustomerId: int
    Items: OrderItem list
    Total: decimal
    Status: OrderStatus
}

and OrderStatus =
    | Draft
    | Validated
    | Paid
    | Shipped
    | Cancelled

type ValidationError =
    | EmptyOrder
    | InvalidQuantity of int
    | CustomerNotFound of int

// Operations.fs - Pure business logic
module OrderManagement.Operations

open Domain

let calculateTotal (items: OrderItem list) : decimal =
    items
    |> List.sumBy (fun item -> decimal item.Quantity * item.Price)

let validateOrderItems (items: OrderItem list) : Result<OrderItem list, ValidationError> =
    if List.isEmpty items then
        Error EmptyOrder
    else
        let invalidItem = items |> List.tryFind (fun i -> i.Quantity <= 0)
        match invalidItem with
        | Some item -> Error (InvalidQuantity item.ProductId)
        | None -> Ok items

let applyDiscount (order: Order) : Order =
    let discount =
        if order.Total > 100M then 0.1M
        elif order.Total > 50M then 0.05M
        else 0M
    { order with Total = order.Total * (1M - discount) }

// Workflows.fs - Use cases orchestration
module OrderManagement.Workflows

open Domain
open Operations

type Dependencies = {
    findCustomer: int -> Async<Option<Customer>>
    saveOrder: Order -> Async<Result<int, string>>
    sendEmail: string -> string -> Async<unit>
}

and Customer = { Id: int; Email: string; Name: string }

let createOrder (deps: Dependencies) (customerId: int) (items: OrderItem list) : Async<Result<Order, ValidationError>> =
    async {
        // Validate items (pure)
        match validateOrderItems items with
        | Error e -> return Error e
        | Ok validItems ->
            // Check customer exists (effect)
            let! customerOpt = deps.findCustomer customerId
            match customerOpt with
            | None -> return Error (CustomerNotFound customerId)
            | Some customer ->
                // Create and calculate (pure)
                let total = calculateTotal validItems
                let order = {
                    Id = 0
                    CustomerId = customerId
                    Items = validItems
                    Total = total
                    Status = Draft
                }
                let discountedOrder = applyDiscount order
                
                // Save (effect)
                let! saveResult = deps.saveOrder discountedOrder
                match saveResult with
                | Error msg -> return Error (CustomerNotFound customerId)
                | Ok orderId ->
                    // Notify (effect)
                    do! deps.sendEmail customer.Email "Order Created"
                    return Ok { discountedOrder with Id = orderId }
    }

// Infrastructure.fs - External implementations
module OrderManagement.Infrastructure

open System.Data.SqlClient
open Domain

let findCustomerDb (connectionString: string) (customerId: int) : Async<Option<Customer>> =
    async {
        // Database implementation
        return Some { Id = customerId; Email = "test@example.com"; Name = "Test" }
    }

let saveOrderDb (connectionString: string) (order: Order) : Async<Result<int, string>> =
    async {
        // Database implementation
        return Ok 123
    }

let sendEmailSmtp (smtpConfig: SmtpConfig) (recipient: string) (body: string) : Async<unit> =
    async {
        // SMTP implementation
        return ()
    }

and SmtpConfig = { Host: string; Port: int }

Explanation: Notice the clear separation:

  • Domain.fs: Only types, zero dependencies
  • Operations.fs: Pure functions operating on domain types
  • Workflows.fs: Orchestrates operations and effects, receives dependencies as parameters
  • Infrastructure.fs: Concrete implementations of effects

This structure makes testing trivialโ€”test pure functions directly, and test workflows with mock dependencies.

Example 2: Dependency Injection with Reader Monad

For complex applications, threading dependencies through every function becomes tedious. The Reader monad solves this elegantly:

type Reader<'env, 'a> = Reader of ('env -> 'a)

let run env (Reader f) = f env

let returnR x = Reader (fun _ -> x)

let bind f (Reader r) =
    Reader (fun env ->
        let a = r env
        let (Reader r2) = f a
        r2 env)

type ReaderBuilder() =
    member _.Return(x) = returnR x
    member _.Bind(r, f) = bind f r
    member _.ReturnFrom(r) = r

let reader = ReaderBuilder()

// Environment holds all dependencies
type AppEnvironment = {
    Database: DatabaseOperations
    EmailService: EmailOperations
    Config: AppConfig
}

and DatabaseOperations = {
    findUser: int -> Async<User option>
    saveUser: User -> Async<unit>
}

and EmailOperations = {
    send: string -> string -> Async<unit>
}

and AppConfig = {
    MaxRetries: int
    Timeout: int
}

and User = { Id: int; Name: string; Email: string }

// Access dependencies within Reader context
let getUserWorkflow userId = reader {
    let! env = Reader id  // Get environment
    let! userOpt = env.Database.findUser userId |> Async.RunSynchronously |> returnR
    match userOpt with
    | Some user ->
        do! env.EmailService.send user.Email "Welcome!" |> Async.RunSynchronously |> returnR
        return Some user
    | None -> return None
}

// Usage
let env = {
    Database = { findUser = findUserDb; saveUser = saveUserDb }
    EmailService = { send = sendEmailSmtp }
    Config = { MaxRetries = 3; Timeout = 5000 }
}

let result = run env (getUserWorkflow 42)

Explanation: The Reader monad automatically threads the environment through all operations. Functions declare what dependencies they need by accessing the environment, but callers only provide the environment once at the top level.

Example 3: Event Sourcing Architecture

Event sourcing stores state changes as events rather than current state. This fits functional architecture beautifully:

// Domain events (pure data)
type OrderEvent =
    | OrderCreated of orderId:int * customerId:int * items:OrderItem list
    | OrderValidated of orderId:int
    | PaymentReceived of orderId:int * amount:decimal
    | OrderShipped of orderId:int * trackingNumber:string
    | OrderCancelled of orderId:int * reason:string

// State is derived from events (pure function)
let applyEvent (state: Order option) (event: OrderEvent) : Order option =
    match event, state with
    | OrderCreated (id, customerId, items), None ->
        Some {
            Id = id
            CustomerId = customerId
            Items = items
            Total = calculateTotal items
            Status = Draft
        }
    | OrderValidated orderId, Some order when order.Id = orderId ->
        Some { order with Status = Validated }
    | PaymentReceived (orderId, amount), Some order when order.Id = orderId ->
        Some { order with Status = Paid }
    | OrderShipped (orderId, tracking), Some order when order.Id = orderId ->
        Some { order with Status = Shipped }
    | OrderCancelled (orderId, _), Some order when order.Id = orderId ->
        Some { order with Status = Cancelled }
    | _ -> state

// Rebuild state from event history (pure)
let rehydrate (events: OrderEvent list) : Order option =
    events |> List.fold applyEvent None

// Command handlers produce events (business logic)
let handleCreateOrder (customerId: int) (items: OrderItem list) : Result<OrderEvent list, string> =
    match validateOrderItems items with
    | Error e -> Error (sprintf "Validation failed: %A" e)
    | Ok validItems ->
        let orderId = generateOrderId()  // side effect, but minimal
        Ok [ OrderCreated (orderId, customerId, validItems) ]

let handleValidateOrder (order: Order) : Result<OrderEvent list, string> =
    if order.Status <> Draft then
        Error "Order must be in Draft status"
    else
        Ok [ OrderValidated order.Id ]

// Event store (infrastructure)
type EventStore = {
    save: int -> OrderEvent list -> Async<unit>
    load: int -> Async<OrderEvent list>
}

let processCommand (store: EventStore) (orderId: int) (command: Order -> Result<OrderEvent list, string>) : Async<Result<unit, string>> =
    async {
        // Load history
        let! events = store.load orderId
        let currentState = rehydrate events
        
        // Execute command
        match currentState with
        | None -> return Error "Order not found"
        | Some order ->
            match command order with
            | Error e -> return Error e
            | Ok newEvents ->
                // Save new events
                do! store.save orderId newEvents
                return Ok ()
    }

Explanation: Event sourcing separates:

  • Events: Immutable facts about what happened
  • State: Derived by folding events (pure function)
  • Commands: Business logic that produces events
  • Event Store: Infrastructure for persistence

This provides complete audit history and makes testing simpleโ€”just verify correct events are produced.

Example 4: Capability-Based Security

Instead of checking permissions everywhere, capabilities are functions that can only be created with proper authorization:

// Capabilities are functions that prove authorization
type DeleteUserCapability = int -> Async<Result<unit, string>>
type ViewUserCapability = int -> Async<Result<User, string>>
type UpdateUserCapability = int -> User -> Async<Result<unit, string>>

// Capability provider (infrastructure)
module Capabilities =
    let authorize (db: DatabaseOperations) (currentUser: User) : UserCapabilities =
        {
            deleteUser =
                if currentUser.IsAdmin then
                    Some (fun userId -> db.deleteUser userId)
                else
                    None
            viewUser =
                Some (fun userId ->
                    if currentUser.IsAdmin || currentUser.Id = userId then
                        db.findUser userId
                    else
                        async { return Error "Unauthorized" })
            updateUser =
                Some (fun userId userData ->
                    if currentUser.Id = userId then
                        db.saveUser userData
                    else
                        async { return Error "Unauthorized" })
        }

and UserCapabilities = {
    deleteUser: DeleteUserCapability option
    viewUser: ViewUserCapability option
    updateUser: UpdateUserCapability option
}

// Business logic uses capabilities
let deleteUserWorkflow (capabilities: UserCapabilities) (userId: int) : Async<Result<unit, string>> =
    async {
        match capabilities.deleteUser with
        | None -> return Error "You don't have permission to delete users"
        | Some deleteUser ->
            let! result = deleteUser userId
            return result
    }

// Usage
let currentUser = { Id = 5; Name = "Alice"; Email = "alice@example.com"; IsAdmin = true }
let capabilities = Capabilities.authorize dbOps currentUser

let result = deleteUserWorkflow capabilities 42 |> Async.RunSynchronously

Explanation: By making capabilities functions that can only be constructed with proper authorization, we:

  • โœ… Centralize security logic
  • โœ… Make unauthorized operations impossible (type-safe)
  • โœ… Simplify business logic (no permission checks needed)

If you have the capability, you're authorizedโ€”the type system enforces it!

Common Mistakes โš ๏ธ

Mistake 1: Mixing Pure and Impure Code

โŒ Wrong:

let processOrder order =
    let total = calculateTotal order.Items
    saveToDatabase order  // Side effect in "pure" function!
    total

โœ… Correct:

let processOrder order =
    let total = calculateTotal order.Items
    { order with Total = total }

// Separate function for effects
let saveProcessedOrder order =
    let processed = processOrder order
    saveToDatabase processed

Why: Mixing effects with logic makes testing hard and reasoning difficult. Keep them separate!

Mistake 2: Over-Using Computation Expressions

Computation expressions (like async, result) are powerful but can make code harder to follow:

โŒ Wrong (nested computation expressions):

let workflow = async {
    let! result1 = async {
        let! inner = async {
            return! someOperation()
        }
        return inner
    }
    return result1
}

โœ… Correct:

let workflow = async {
    let! result = someOperation()
    return result
}
// Or simply:
let workflow = someOperation()

Mistake 3: Making Everything a Module Function

Not everything needs to be a standalone module function. Related functions should be grouped:

โŒ Wrong:

module UserOperations =
    let validateEmail email = ...
    let validateAge age = ...
    let validateName name = ...
    // 20 more validation functions

โœ… Correct:

module UserOperations =
    module Validation =
        let email e = ...
        let age a = ...
        let name n = ...
    
    let validateUser user =
        { user with
            Email = Validation.email user.Email
            Age = Validation.age user.Age
            Name = Validation.name user.Name }

Mistake 4: Not Using Type Aliases for Function Signatures

Complex function signatures become unreadable quickly:

โŒ Wrong:

let processWorkflow
    (f1: int -> Async<Result<User, string>>)
    (f2: User -> Async<Result<Order, string>>)
    (f3: Order -> Async<Result<unit, string>>)
    (id: int) =
    // implementation

โœ… Correct:

type FindUser = int -> Async<Result<User, string>>
type CreateOrder = User -> Async<Result<Order, string>>
type SaveOrder = Order -> Async<Result<unit, string>>

let processWorkflow (findUser: FindUser) (createOrder: CreateOrder) (saveOrder: SaveOrder) (id: int) =
    // implementation

Mistake 5: Ignoring Error Handling

โŒ Wrong:

let workflow userId =
    let user = findUser userId |> Async.RunSynchronously |> Option.get  // Crash!
    processUser user

โœ… Correct:

let workflow userId = async {
    let! userOpt = findUser userId
    match userOpt with
    | None -> return Error "User not found"
    | Some user ->
        let processed = processUser user
        return Ok processed
}

Key Takeaways ๐ŸŽฏ

  1. Functional Core, Imperative Shell: Keep business logic pure, push effects to boundaries
  2. Dependency Injection: Pass dependencies as function parameters or use Reader monad
  3. Layer by Feature: Organize code by business domain, not technical layer
  4. Railway-Oriented Programming: Use Result types and composition for error handling
  5. Immutability Everywhere: Return new values instead of mutating state
  6. Types for Design: Use type aliases and domain types to make architecture explicit
  7. Test Pure Functions: Most of your tests should be simple assertions without mocks
  8. Capabilities for Security: Make authorization type-safe by encoding permissions as functions

๐Ÿ“š Further Study

๐Ÿ“‹ Quick Reference Card

PatternPurposeKey Benefit
Functional Core / Imperative ShellSeparate pure logic from effectsEasy testing, reasoning
Railway-Oriented ProgrammingError handling in pipelinesComposable error flow
Reader MonadDependency injectionImplicit environment threading
Onion ArchitectureLayer separationDependencies point inward
Event SourcingState from event historyAudit trail, time travel
Capability-Based SecurityType-safe authorizationCannot call unauthorized operations
Feature OrganizationGroup by domainLow coupling, high cohesion

๐Ÿง  Memory Device - FRICORE:

  • Functional core
  • Railway-oriented errors
  • Immutable state
  • Composable functions
  • Onion layers
  • Reader for dependencies
  • Effects at edges

Practice Questions

Test your understanding with these questions:

Q1: Complete the function to make the validation pure: ```fsharp let validateAge age = if age < 18 then {{1}} "Too young" else {{2}} age ```
A: ["Error","Ok"]
Q2: What is the primary benefit of the Functional Core, Imperative Shell pattern? A. Faster execution speed B. Easier to test business logic C. Reduces memory usage D. Better database performance E. Automatic parallelization
A: B
Q3: Complete the Railway-Oriented Programming bind function: ```fsharp let bind f result = match result with | Ok value -> {{1}} | Error e -> {{2}} ```
A: ["f value","Error e"]
Q4: In F# functional architecture, dependencies should be passed as {{1}} rather than injected through constructors.
A: parameters
Q5: What does this code demonstrate? ```fsharp type Dependencies = { findUser: int -> Async<User option> saveUser: User -> Async<unit> } let workflow (deps: Dependencies) userId = async { let! user = deps.findUser userId return user } ``` A. Dependency injection via function records B. Event sourcing pattern C. Mutable state management D. Object-oriented inheritance E. Database connection pooling
A: A