December 20, 2025
/ Architecting a TUI: How Open-Book Taught Me Design Patterns
When scripts become unmanageable, you need architecture. Here is how building Open-Book — a terminal-based RAG system — became my ultimate playground for mastering design patterns, CLIs, and TUIs.
Prototyping an AI tool in a Jupyter Notebook is easy. Turning that prototype into a maintainable, interactive, and fast application is a different beast entirely.
When I set out to build Open-Book — a local Retrieval-Augmented Generation (RAG) system for chatting with technical PDF libraries — I realized my notebook scripts wouldn't scale. Moving the application strictly to the terminal to create an interactive TUI (Terminal User Interface) forced me to rethink my entire architecture, finally putting textbook design patterns into practical use.
The Problem with Spaghetti Scripts
RAG systems are inherently complex. You have PDF parsers, text splitters, embedding models, vector stores, and the LLM generation logic.
Initially, these components were tightly coupled. If I wanted to switch the embedding model or change how documents were chunked, I had to rewrite the core execution pipeline. To build a robust CLI and an interactive TUI on top of this engine, I needed decoupling.
Enter Design Patterns
Building Open-Book became a crash course in utilizing object-oriented design patterns to keep the codebase clean.
The Strategy Pattern for Dynamic Processing
Different documents require different ingestion strategies. A technical textbook is processed differently from a code manual. By implementing the Strategy Pattern, I could define a common interface for document processing and hot-swap the concrete implementation (e.g., PDFChunker vs. MarkdownChunker) seamlessly without altering the core indexer.
The Factory Pattern for Model Adapters
LLM backends shift rapidly. Today it's a local Llama model via Ollama, tomorrow it might be an API hook. I used the Factory Method to instantiate the correct model adapter based on user configuration. The TUI doesn't need to know what model it's speaking to — only that the model adheres to the generate_response() interface.
The Observer Pattern for TUI State
Building a TUI requires reactive state. When the backend vector search finds a relevant document, the visual interface needs to update the chat pane, show citations, and maybe display a loading spinner. The Observer Pattern allowed the UI components to subscribe to backend events. The RAG pipeline broadcasts a ChunkRetrieved or TokenGenerated event, and the terminal widgets update asynchronously.
Why a TUI?
Command Line Interfaces (CLIs) are great for single-shot commands, but chatting with a library requires persistent context. I wanted the speed and minimal distraction of the terminal, but the interactive feedback of a web app.
Building the Terminal User Interface meant handling keystrokes, rendering syntax-highlighted markdown blocks directly in the console, and managing scrollable layout panels. It dramatically shifted my perspective on what terminal tools are capable of. It’s no longer just printing stdout; it’s painting pixels with text characters.
The Payoff
The most valuable takeaway from building Open-Book wasn't just having a fast, offline tool to query my textbooks. It was the realization that design patterns aren't just academic exercises for Java developers. They are the exact tools required to orchestrate complex data flows — like LLM pipelines and user interfaces — without losing your mind.
If you are looking to level up your systems architecture, take a messy Jupyter notebook script and rewrite it as a fully-featured interactive TUI.