Gym App - a case in business logic
Tony Jiang · · 7 min read · permalink
Scroll to the bottom for pictures.
PRDs and the Business Logic Problem
For a very long time, what distinguished amateur from professional projects was the depth of business logic — how many cases it could handle, how well thought out it was in terms of handling exceptional behavior. This was largely inaccessible to amateurs, as it required a lot of manual reasoning and typing. Now, it seems the right tools exist to crack it.
How my journey started
Initially I started with the hazy idea of “writing product.” In my experiences with Claude Code, I realized I was able to get a scaffold working quickly, but not the extensive business logic. That’s when I came across the concept of PRDs, or product requirement documents.
From my own experience I did intuit that you needed to create different types of documents like “entities” and “workflows” (which I would categorize as subtypes of PRDs), or at least have these in mind before starting. I tried using Claude Code directly to create PRDs for a concept app, a makerspace with classes, certifications, and bookable machines. This ended up being very verbose, with a seemingly unlimited number of rounds of questioning.
Then, flipping through old YouTube history, I realized that a whole system had already been created by Matt Pocock with his spec-driven development approach. It consists of downloading skills/plugins from his repo into Claude Code, and then using various commands: /grill-me, /to-spec, /to-tickets, and /implement.
Picking a project that was actually tractable
My initial idea, the makerspace, proved to be too complicated for plain Claude Code, and possibly still too complicated even with Matt’s plugin. I did not give up. I thought about a smaller project (gym classes) to prove feasibility, which turned out to have enough of its own complexities that it ended up being better than a toy case. Tractable but also challenging and educational, the sweet spot.
The workflow
The pipeline, end to end:
- Run Pocock’s skill setup. This created a directory structure with initial documents.
- Describe the product. I said I wanted a boutique gym/studio class booking system, and supplied a generated description with the main entities and basic business rules.
/grill-me: several rounds of clarifying questions./to-spec: where I had to answer the questions that were missed in the first round./to-tickets: which created 12 separate tickets./implement, once per ticket.
Some of this only became obvious once I was in it.
At the ticket stage I had to clarify that I wanted a frontend with a backend. There’s often multiple ways of splitting things up, so you have to be strategic about this (for instance, you could write them separately, or write them together).
Halfway through /implement, I realized there was a lot of testing happening, like setting up and tearing down test servers, so I told it to stop the tests so it would move faster. When it finished, it did give me some bugs that it had run into while writing the code. So I do think it’s necessary to run tests, although it can be a bit excessive at times.
At the end I was able to use the test data accounts to verify that it worked, and it also created an admin board. It also wrote many tests using Vitest.
The questions it asked me
/grill-me asks with a position already staked out, so you spend your time overruling a proposal rather than filling in a blank. Four examples of what it put to me:
Is Waitlist an entity, or a state a Booking is in?
You listed Waitlist as an entity. My argument for it being a state instead: promotion becomes one status transition on one row rather than a delete-and-create across two concepts, and “a Member can’t hold two places in the same Session” becomes a single uniqueness rule instead of two that must agree.
One studio, or several locations?
Multi-location pulls in a Location entity, per-location capacity, instructors who work across sites, and real timezone arithmetic on every session time. It’s the single biggest scope multiplier available here.
Is losing the credit the entire penalty, or is there an access penalty on top?
This is the parent of your whole no-show branch, so it comes before any ladder mechanics. A no-show already forfeits the credit automatically — they booked, they didn’t cancel, the credit is gone. So the question is whether anything further happens. The argument for “credit forfeiture is enough”: it’s self-enforcing, needs no new state, and the member has already paid for the harm. The argument against: the real harm isn’t the credit, it’s that a held seat sat empty while someone on the waitlist was turned away — and a member with a large balance can absorb that cost indefinitely.
Beyond closing out the roster, what can an instructor do?
They already have attendance marking. The remaining candidates: view the roster (names) for their own upcoming sessions; cancel their own session; hand a session to a substitute; see their own teaching schedule.
What I learned
Writing the business down
In the past, business logic was looked down upon, or at least somewhat not formalized. And I could see why. You’d have to write it down, which was much more tedious. With the release of these new skills, having a formally written-down business has introduced a new set of possibilities and challenges.
PRDs, entities, rules, and workflows are the primary types of documents this generates, but there are many more ways to specify product, like tables and trees, that could come in handy depending on the project.
Rules are pairs of conditions and behaviors: when you see this, do this. For example, “any confirmed Booking left unmarked at close-out becomes a no_show. If a Session is never closed out at all within twenty-four hours, every confirmed Booking is automatically marked attended.” This is ever-present, so the system must be able to detect when the rule is being triggered.
This project also gave me a close look at the challenges around transactions. Transactions must be atomic, and when reversed they can affect many other things downstream (that is, other objects).
Slicing matters
There are many different ways to break a problem into steps, and some ways are more tractable than others. In this case I had the frontend and backend written separately, because it was simple enough that you didn’t need to test while building.
Tickets are also not evenly sized. The first ticket was the biggest, and the later ones got smaller once the groundwork had been laid.
Decisions worth front-loading
It’s better to have opinions about the tech stack at the beginning, since these are highly consequential. I chose Postgres, Fastify, React, Vite, TypeScript, shadcn, and Tailwind. Leaving that later, I believe, could have led to derailment.
The main decisions are the tech stack, ideally something you choose yourself up front, and the business logic, which is what /grill-me is for. Doing things right from the start is very important to avoid a later onslaught of questioning. I had to specify my desired tech stack, and to direct it to create a Postgres schema at the beginning.
Friction and proportion
Sometimes you need to turn off test writing, because it burns through credits and it’s not always the most efficient use of resources. I’ve since noticed analogous situations in many other Claude workflows.
Manual state sync is also required, like updating the status of tickets after completion. This is a bit of a pain point, though I can see it being fixed in the future.
Skills, and the new bottleneck
This was the first time I used a skill plugin, which opened my eyes to how necessary it is for certain workflows to have the benefits that skills bring.
It also moved where the work is. Comprehension and reading are now the big bottlenecks, since they’ve been decoupled from writing.
Where judgment still lives
I find judgment really takes place based off irreplaceable human skills: having a sense of proportion (we don’t need that many tests), knowing implicit requirements (calling out when something is not right), and having a better model of where things are going (we’re likely going to need to go beyond just using Tailwind as the app grows in complexity, for instance).
What’s next
Test writing has become far easier. The difficulty of writing tests in the past was knowing enough business logic to create accurate setup data, which was always a very tedious task, and that’s now been resolved. This makes it easier to create and test isolated units before wiring them up into a system.
Extension, maintenance, and refactoring are all separate tasks with their own workflows, and they’re the real mark of long-term maintainability of a system.
Pocock’s plugin has many other skills, like /wayfinder, that I want to incorporate. The current project wasn’t exactly a happy path, but it was not that unusual or complicated either.
Pictures