How to design an App (and not hate yourself)



Many times I’ve started a project, raring to go, and after a few days realized I wished I thought out the design of the damn thing better. I’ll always start out with a small proof of concept that works, then I’ll add a feature here and a feature there and the app will become spaghetti code, even in less than 1000 lines of code. Or maybe I’ll come back to a project after a few years and realize I can’t understand what I was trying to do, or the features I was trying to add, or the bugs I needed to fix.

I decided to take a stand and come up with some structure that would help me tackle these problems.

Document your thoughts

I don’t frequently comment my code, mainly from my years working in corporate. I brought that habit over to my personal projects, because I believe the code itself should be written in a way that intent is clear. What’s not clear, however, is why you chose to do something one way over any other way, and the trade-offs that entailed. For that, I’ll write an architecture file that explains some of the decisions, goals, and explicit non-goals. This document is invaluable when coming back to a project.

As well, tests are great for jogging the mind. If I write a library, I find myself wanting to run it to see what it did and where I left off. Tests let me do that – I can read the tests and see what features/bugs the library has. I’ll write some failing tests and a comment that explains the feature or bug briefly, and leave it failing. When I come back to it, I’ll try to fix it up.

On top of traditional testing, I’ve recently fallen in love with fuzzing my code. Fuzzing involves generating random structured data and then calling a function in your program, and trying to crash (or otherwise trigger a bad state). I recently wrote a bitcask database in Rust, and when I thought it was satisfactory, I wrote up a few unit tests and I started fuzzing my app. In less than a second it found 3 bugs, checking code paths that would be difficult to do by hand. These test cases are then easily transferable to your normal unit testing suite, which can act like a regression suite.

Managing State

The second part I find difficult about writing a project is managing state – If I start out with a small core and adding features to it in an unstructured way, I tend to end up with a jumbled mess with state everywhere. The frontend world took a nod from the functional world and decided to keep state in stores, where state and state management routines are housed, and the rest of the application has to call into the stores to get or set specific values. These values are also purely functional, which allows for time-travel debugging, where you can go back in time to a point where the app was at a certain state. Gary Bernhardt took this further and espoused a philosophy called “functional core, imperative shell”. Any part of the application that can be pure, or only rely on its arguments is easy to test and hard to get wrong. As much of as possible of the program should be written in this style. Any part of the program that is required to deal with state, or has side effects (network requests, Disk I/O) should be shunted around the functional core, and use integration tests. This is kind of like MVC, where the controllers and the model are the imperative shell, making network requests or doing file I/O, and the view shuffles data between both of them.

This breaks your application up into modules based on how much they interact with state. This is a big win for understanding your code after a break – the duties of each module is clearer, and testing duties are also clearer, since you don’t have to stub any part of the system out, just integration test + fuzz the boundaries of the code and fuzz + unit test the functional core.

Having fun

Normally I dislike structure, instead doing things that I enjoy without strictly adhering to rules. What I found interesting was that having just the right amount of structure increased the amount of enjoyment I got from coding and other projects. I didn’t have to spend as much time reimmersing myself in the project, which I found boring and a blocker to coming back to hard projects. It also made me a better programmer, since testing was easier and I could spot out troublesome patterns of code and became more efficient at testing. Also, I started tackling harder projects because I could build things up incrementally faster and not hit the cognitive wall where making a change was higher than the amount of enjoyment I would get from said change.

The right amount of structure is like the right amount of spice. It makes everything taste better.