How I Turned a TUI Failure into a Backend Architectural Win with Python

The Initial Idea
I've been using a written Bullet Journal for many years now. Full disclosure: I am by no means someone who spends hours upon hours beautifying their "BUJO"—I leave that to YouTubers and notebook enthusiasts. I will say though, it has been the single most effective productivity tool for me personally, and I would highly recommend it to anyone.
So, when it came time for my first personal project in the Backend Developer Career Path on Boot.dev, I thought it would be a fairly decent concept to make a TUI (Terminal User Interface) application inspired by Ryder Carroll's Bullet Journal Method. I initially envisioned a beautiful, easy-to-use TUI app. However, unbeknownst to me, the TUI style of application would bring a lot of unexpected complexity. Chalk it up to naivety!
In the end, the project became like my handwritten bullet journal: simple in execution but immensely helpful. What follows is an account of pivoting from a TUI application to a highly effective CLI (Command Line Interface) application, and how that pivot resulted in a major backend architectural win.
The Challenge
At first, I wanted a TUI to display the bullet journal in a dynamic and interesting way. After implementing the data structure and the core logic of the application, I found that I had created tightly coupled code. This led to a myriad of rendering issues and constantly fighting with the library to behave as I had expected.
The core of the issue was not the UI presentation, but that I was coupling the data access (JSON I/O) directly into the application logic. After battling this for hours and trying to use both textual and urwid, both to no avail, I decided I needed to make a difficult change. I realized I was wasting time on the UI when I actually needed to be focusing on the backend architecture.
The Architectural Pivot: Defining the Contract
So it was time to pivot. Since my goal is to be a backend developer, I decided the most critical requirement was architectural design. In short, I needed to make sure that the main application (app.py) didn't care how the data was saved—whether it was JSON, SQLite, or Postgres. This was much more important to me at the time than fighting and struggling with libraries.
I chose a simple JSON file for the Minimum Viable Product (MVP). This allowed me to focus on the core logic, and along with the decoupling, it would allow me to later implement a proper database if necessary. To ensure data persistence, I defined a JournalRepository Protocol:
class JournalRepository(Protocol):
"""
Repository interface for journal data persistence.
"""
def load_entries(self) -> List[JournalEntry]:
"""Load all journal entries from storage."""
...
def save_entries(self, entries: List[JournalEntry]) -> None:
"""Save all journal entries to storage."""
def find_by_date_range(self, start: datetime, end: datetime) -> List[JournalEntry]:
"""Find entries within a specific date range."""
...
The Implementation: Adhering to the Protocol
The architectural key here is the Repository Pattern. My goal was to create a contract for any data persistence layer. This contract is the JournalRepository Protocol you see above.
I then created a concrete class, FileJournalRepository, located in persistence.py. This class knows only two things:
- How to handle reading and writing to a JSON file.
- That it must satisfy the contract defined in the JournalRepository Protocol.
By forcing the implementation class to adhere to the Protocol, I can write clean, testable code that is completely isolated from the main application logic. The repository is the only part of the application that knows about the file system. If the load_entries function in FileJournalRepository breaks, I know exactly where to look, and the rest of the application remains untouched.
The Payoff: A Truly Decoupled Application
The real payoff is that the BulletJournalCLI in my app.py only depends on the interface, not the specific file implementation. The application code looks something like this:
# app.py
class BulletJournalCLI:
def __init__(self, repository: JournalRepository):
self.repository = repository # Accepts ANY class that implements JournalRepository
def run(self):
entries = self.repository.load_entries()
# ... application logic
The BulletJournalCLI doesn't know (or care!) if the data is coming from a local JSON file or a server in the cloud. All it knows is that the object in self.repository has a load_entries method.
This single architectural pivot, born from the frustration of a TUI failure, transformed my project. It made my code extensible, testable, and maintainable. I'm no longer trapped by my initial choice of storage. Swapping out the simple JSON storage for a Postgres database or even a cloud API now requires changing only one file (persistence.py)—the one that implements the JournalRepository contract. That's a huge win, and it's an essential skill for building microservices and complex, enterprise-level systems.





