Skip to main content
Bubble Tea is built on The Elm Architecture, a functional design pattern that provides a clean, predictable way to build interactive applications. This architecture separates concerns beautifully and creates a unidirectional data flow that makes your application easy to reason about.

Core Principles

The Elm Architecture is based on three core principles:
  1. Single Source of Truth: All application state lives in one place (the Model)
  2. Unidirectional Data Flow: Data flows in one direction through the application
  3. Pure Functions: State changes are predictable and testable

The Model-Update-View Cycle

Every Bubble Tea application follows this cycle:
1

Model

Your application’s state. This is the single source of truth for what your app knows and displays.
2

View

A pure function that takes the model and returns what to display. The view never modifies the model.
3

Update

Handles messages (events) and returns a new model. This is where state changes happen.
4

Commands

Optional I/O operations that return messages. Commands handle side effects like HTTP requests or timers.

Unidirectional Data Flow

Data flows in one direction through your application: This unidirectional flow means:
  • The View never modifies the Model directly
  • The Model never calls the View directly
  • All state changes go through Update
  • Side effects are isolated in Commands

Architecture Diagram

Here’s how the components work together:
┌─────────────────────────────────────────────┐
│                                             │
│  ┌──────────┐      ┌──────────┐            │
│  │   Init   │─────▶│   Cmd    │            │
│  └──────────┘      └──────────┘            │
│       │                  │                  │
│       │                  │                  │
│       ▼                  ▼                  │
│  ┌──────────┐      ┌──────────┐            │
│  │  Model   │      │   Msg    │            │
│  └──────────┘      └──────────┘            │
│       │                  │                  │
│       │                  │                  │
│       ▼                  ▼                  │
│  ┌──────────┐      ┌──────────┐            │
│  │   View   │◀─────│  Update  │────┐       │
│  └──────────┘      └──────────┘    │       │
│       │                             │       │
│       │                        ┌────▼────┐  │
│       ▼                        │   Cmd   │  │
│  ┌──────────┐                 └─────────┘  │
│  │  Render  │                      │        │
│  └──────────┘                      │        │
│                                    │        │
│                              (loops back)   │
│                                             │
└─────────────────────────────────────────────┘

How It Works

1. Initialization

When your program starts, Bubble Tea calls Init() on your model:
func (m model) Init() tea.Cmd {
    // Return an initial command or nil
    return nil
}
See tea.go:52-55

2. The Update Loop

When something happens (a key press, timer tick, HTTP response), Bubble Tea:
  1. Wraps it in a Msg (message)
  2. Passes it to Update() along with the current model
  3. Gets back a new model and optional command
  4. Calls View() with the new model
  5. Renders the result
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // Handle key presses
        if msg.String() == "q" {
            return m, tea.Quit
        }
    }
    return m, nil
}
See tea.go:57-60

3. Rendering

The View() method is called after every update:
func (m model) View() tea.View {
    return tea.NewView("Hello, World!")
}
See tea.go:61-64

Benefits

Predictable

Same input always produces same output. No hidden state changes.

Testable

Pure functions are easy to test. No mocking required for most logic.

Debuggable

Clear sequence of events. Easy to trace how state changes.

Composable

Models, updates, and views can be composed from smaller pieces.

Example: Counter Application

Here’s a complete example showing the architecture in action:
package main

import (
    "fmt"
    tea "charm.land/bubbletea/v2"
)

// 1. Model: Application state
type model struct {
    count int
}

// 2. Init: Initial command (none needed here)
func (m model) Init() tea.Cmd {
    return nil
}

// 3. Update: Handle messages and update state
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "up":
            m.count++
        case "down":
            m.count--
        case "q", "ctrl+c":
            return m, tea.Quit
        }
    }
    return m, nil
}

// 4. View: Render the UI
func (m model) View() tea.View {
    s := fmt.Sprintf("Count: %d\n\nPress up/down to change, q to quit.", m.count)
    return tea.NewView(s)
}

func main() {
    p := tea.NewProgram(model{count: 0})
    if _, err := p.Run(); err != nil {
        fmt.Printf("Error: %v", err)
    }
}

Why The Elm Architecture?

Since all state changes go through Update, you can’t have two parts of your code modifying state simultaneously.
Because updates are pure functions, you can replay the exact sequence of messages to reproduce any state.
Commands handle I/O concurrently, but results come back as messages through the same Update function.
The pattern maps naturally to Go’s strengths: simple types, clear interfaces, and explicit error handling.

Key Takeaways

The Elm Architecture gives you:
  • One way to change state (through Update)
  • One place to handle events (the Update function)
  • One source of truth (the Model)
This simplicity makes complex applications manageable.

Next Steps

Model

Learn about the Model interface and how to structure your application state

Messages

Discover how messages flow through your application

Commands

Understand how to perform I/O operations

Views

Master rendering your application’s UI

Build docs developers (and LLMs) love