what i learned today while designing a python cli book manager
a simple project that turned into a system design exercise — and all the small decisions that shape a program before most of the code exists.
today started with something that looked simple on the surface.
build a CLI book manager in python.
- add a book
- list books
- find by author
- remove a book
the kind of program you could probably make run pretty quickly. but instead of just writing something that works, i tried thinking about it more like a small system design exercise.
once you start looking at it that way, a lot of small design decisions begin to appear. things that don't look important at first but actually shape the whole program.
data structures quietly control everything
the first thought most people have when storing multiple items in python is a list.
books = [
{"title": "Clean Code", "isbn": "9780132350884"},
{"title": "The Pragmatic Programmer", "isbn": "9780201616224"}
]
this works fine if the only thing you want to do is store and display books.
but the moment you want to find a specific book, problems start showing up. for example, finding a book by ISBN:
def find_book(isbn):
for book in books:
if book["isbn"] == isbn:
return book
the program has to check every single entry until it finds the match. this is an O(n) operation. the time grows as the number of books grows.
a better design is to make ISBN the key of a dictionary instead.
books = {
"9780132350884": {"title": "Clean Code", "author": "Robert C. Martin"},
"9780201616224": {"title": "The Pragmatic Programmer", "author": "Andrew Hunt"}
}
now finding a book becomes direct:
book = books["9780132350884"]
python jumps straight to the value instead of scanning the whole dataset. this changes the lookup from O(n) to O(1). same feature. very different performance.
and the interesting part is that this decision happens before any real features are written.
some searches will always be slower
once the dictionary was keyed by ISBN, another question appeared. what about find by author or find by title?
a dictionary doesn't help much here because those fields are inside the values, not the keys. so the only option is to iterate and filter.
def find_by_author(author):
return [b for b in books.values() if b["author"] == author]
this is still O(n). initially that might feel inefficient. but context matters.
a CLI program like this might store a few hundred books at most. scanning a few hundred entries takes almost no time. adding complex indexing structures would only make the program harder to maintain.
so sometimes the better engineering choice is keeping the design simple instead of optimizing everything.
humans are bad identifiers
another design decision showed up while thinking about deletion. how should a book be removed?
the first instinct is usually remove by title, because that's what users remember. but titles are not unique.
- different books can share the same name
- new editions exist
- reprints exist
authors are not reliable either. one author can write many books. the only safe identifier here is ISBN.
this pattern appears everywhere in real systems. what users see is usually a human-friendly label. what systems store internally is a unique identifier. users see names, titles, and emails. systems use UUIDs, primary keys, and IDs.
CLI programs still need structure
a common pattern in small scripts is something like this:
while True:
if choice == "1":
...
this works for tiny programs. but as the number of commands grows, the logic quickly becomes difficult to maintain. a cleaner approach is separating responsibilities.
# CLI layer → handles user input
# service layer → handles book operations
# storage layer → handles persistence
def main():
while True:
cmd = get_input()
router.dispatch(cmd)
this small structural change makes the program easier to extend later.
persistence changes the design
initially the books only existed in memory, which means once the program exits, all data disappears.
for a tool like this, SQLite fits nicely. it runs in a single file and requires almost no setup. but the more interesting decision is how the program interacts with the database.
# option A: query the database every time
def find_book(isbn):
return db.query("SELECT * FROM books WHERE isbn=?", isbn)
# option B: load everything once at startup
def startup():
books = db.load_all()
return books
for a small CLI tool, the second approach is usually simpler. the program loads everything at startup, works in memory, and only touches the database when writing changes.
in this setup: database is the source of truth. memory is the working cache. this pattern appears in many real systems.
consistency issues are subtle
what happens if memory updates but the database write fails?
# incorrect order
books[isbn] = new_book
db.insert(new_book) # if this fails, state is inconsistent
# correct order
db.insert(new_book)
if success:
books[isbn] = new_book
small ordering detail, but it prevents inconsistent state.
menus don't scale well
the first CLI idea used alphabet menus.
a) add book
b) list books
c) find book
this works for simple demos, but once commands grow it becomes harder to remember. many professional CLI tools instead use command verbs.
bookcli add "Clean Code" --author "Robert C. Martin"
bookcli list
bookcli find --author "Robert C. Martin"
bookcli remove 9780132350884
tools like git, docker, and kubectl follow this pattern because it scales better.
none of the individual features in this project are complicated. adding a book is straightforward. listing books is straightforward. searching books is straightforward.
but the interesting part wasn't writing the code. it was thinking about the questions behind the code:
- how should the data be structured?
- what needs to be fast and what can remain simple?
- what uniquely identifies a record?
- where should responsibilities live in the program?
- how should memory and persistence interact safely?
these decisions shape the system long before most of the code exists.
looking forward to coding the cli.
what i learnt today is swe often comes down to designing the structure well before implementing the features.