Why Mechanism Design Felt Harder Than Writing Code in My Balatro-like Backend Project
A backend developer’s reflection on moving from feature implementation to state lifecycle, game rules, and system design.

This is my first article on Hashnode.
I am building a Balatro-like card game backend from scratch with Node.js, TypeScript, NestJS, and WebSockets. The project started as a way to practice backend engineering, but it slowly turned into something more interesting: a long-term exercise in state design, game rules, and system evolution.
The source code is available on GitHub: RainbowZhou93/balatro-realtime-backend
Before the code, I got stuck
Before writing the endpoint, I first had to understand the rule, the state, and the lifecycle behind it.
This article is a little different from the usual “I implemented feature X” kind of post.
There is no big code commit behind it. There is no finished endpoint to show. There is no elegant refactor that suddenly solved everything.
Instead, this article is about the moment I got stuck before implementing a feature.
The feature looked simple at first:
Allow the player to skip a Blind and receive a Tag reward.
If you have played Balatro, this idea is familiar. The player can skip a Small Blind or Big Blind, give up the current round, and receive a Tag that creates some future benefit.
From a backend perspective, it sounded like a normal feature: skipBlind(playerId).
Maybe all I needed to do was validate whether the current Blind could be skipped, generate a reward, move the game to the next Blind, and return the updated game state.
Simple, right?
But when I actually started thinking about it, I realized that the hard part was not writing the skipBlind function.
The hard part was answering the questions behind it.
The real questions behind skipBlind
Once I stopped thinking about the endpoint name and started thinking about the rule itself, the problem expanded quickly:
Why would the player want to skip the current Blind?
What should the player receive after skipping it?
If the reward is too weak, why would anyone choose it?
If the reward is too strong, does skipping become the obvious best choice?
Should the reward take effect immediately or later?
Should the reward belong to the current Blind, the next Blind, or the player?
How should the backend remember this choice across the game flow?
When should this temporary effect be cleaned up?
At first, all of these questions looked like details of one feature.
But after thinking about them for a while, I realized they were not just implementation details.
They were design questions — and more importantly, system lifecycle questions.
In my previous work, the boundaries were usually already defined
In my past work, the usual workflow was much clearer.
A product manager or project manager would define the requirement. Then I would implement it.
The requirement might include things like:
What parameters the API needs
What data the frontend expects
What counts as success
What counts as failure
What error cases should be handled
How QA should verify the result
In that kind of workflow, my main question was usually:
How should I implement this requirement correctly?
Not:
Why should this requirement exist in this form?
That does not mean regular business development requires no thinking. Of course it does.
But in many cases, the boundaries of the problem had already been defined by someone else. As a backend developer, I mostly work inside those boundaries and try to make the implementation reliable, maintainable, and easy to extend.
The mental model is often:
Requirement -> Parameters -> Code
This is a familiar path.
You receive the requirement, break it down into data structures and API behavior, then write the code.
A personal project does not define the requirements for you
This Balatro-like backend project is different.
There is no product manager. There is no detailed requirement document. There is no teammate sitting next to me to discuss whether a rule makes sense.
I have to decide many things myself:
What should be implemented in this stage?
How much should this feature be simplified?
Which state should be maintained by the backend?
Which rule can be hard-coded for now?
Which structure should leave space for future systems?
What problem does this mechanism actually solve?
At the beginning, this was not too difficult.
The early features were very concrete:
Shuffle the deck
Deal cards
Play selected cards
Discard cards
Refill the hand
Detect poker hand type
Calculate score
Decide whether the current round is over
These features are still backend logic, but their boundaries are easier to understand.
For example, when a player plays cards, the flow is relatively direct:
Player selects cards
↓
Validate selected cards
↓
Calculate hand score
↓
Update current Blind score
↓
Decrease remaining plays
↓
Refill player hand
There is state involved, but the scope is still centered around one player action.
The action starts, the backend processes it, and the action ends.
That kind of problem is easier to reason about.
Then the project moved from actions to lifecycle
The difficulty changed when the project entered the next stage.
I started working on concepts like:
Blind
Ante
Boss Blind
Skip reward
Tag
Stage progression
Temporary effects
Future shop and economy systems
These are no longer just single actions.
They form a lifecycle.
A Blind has a start. It has a target score. It receives score updates. It can be cleared or failed. It may move the player to the next Blind. It may trigger rewards. It may reset some state but keep other state.
That means the core problem is no longer:
How do I write this function?
It becomes:
Where does this state live, when does it change, and how long should it survive?
That was when the project started to feel like a real system rather than a set of isolated functions.
The hard part was not code complexity, but state relationships
When I implemented basic play-card logic, I could think in a short chain:
Selected cards -> Hand type -> Score -> Remaining plays -> Refill hand
But when I started designing Blind progression, the chain became longer:
Current Blind state
↓
Player action
↓
Score change
↓
Check whether target score is reached
↓
Decide whether the Blind is cleared
↓
Move to the next Blind or next Ante
↓
Reset round-level state
↓
Keep player-level progress
↓
Apply or clean up temporary effects
The difficult part was not that any single step was extremely hard to code.
The difficult part was that every step affected another step.
If I store a Tag in the wrong place, future reward logic becomes awkward. If I apply a reward too early, the game flow becomes confusing. If I move the Blind state too soon, the response returned to the frontend may describe the wrong moment. If I do not distinguish temporary state from persistent player state, the system becomes hard to extend.
That is when I started to feel the thinking order change.
Before, I was mostly thinking like this:
Requirement -> Parameters -> Code
Now I had to think more like this:
Rule -> State -> Lifecycle -> Architecture -> Extensibility
This was much harder.
Not because the code was more advanced, but because the design space became larger.
Why not just copy Balatro’s rules?
This was another question I asked myself.
Balatro is a public game. I can open the game, read guides, check the wiki, and see many mechanics directly.
So why spend so much time thinking about the design?
Why not just copy the result?
For example, I can find out:
What Tags exist in the game
What happens after skipping a Blind
Which rewards are available
How certain effects behave
But I slowly realized that knowing the result is different from understanding the reason.
If I only copy the visible behavior, I may know what to implement.
But I may not understand why it belongs there.
For example, skipping a Blind is not just:
Player skips -> Backend gives reward
It is more like:
Player gives up current value -> Receives uncertain future value
That makes it a risk-and-reward mechanism.
And once I think about it that way, the design questions become more meaningful:
What value is the player giving up?
What kind of future value is attractive enough?
Should the reward affect combat, economy, or deck building?
Should it help immediately or create delayed potential?
How does this choice interact with future systems?
This is the difference between copying a feature and understanding a mechanism.
Copying answers the question:
What happens?
Design tries to answer:
Why should it happen this way?
A small feature can change the backend model
The more I thought about skip rewards and Tags, the more I realized that a small feature can force the backend model to evolve.
A Tag is not just a reward text displayed on the screen.
It may affect:
The next Blind
The next Boss Blind
The player’s hand size
Future shop behavior
Money rewards
Temporary effects
Long-term progression
That means the backend cannot treat it as a simple string.
It needs to know:
Whether the Tag is pending
When the Tag should be used
Whether the Tag has already taken effect
What part of the game state it modifies
When the effect should be removed
This is where backend state design becomes important.
If a piece of data affects future results, it should not be treated as frontend-only display data.
The frontend can display the current Blind, target score, available reward, and player status.
But the backend should be the trusted source for rules such as:
Whether the current Blind can be skipped
Which Tag can be granted
Whether a Tag is active
Whether the game can move to the next stage
Whether a temporary effect should still exist
| Responsibility | Frontend | Backend |
|---|---|---|
| Current Blind display | Yes | Provides trusted state |
| Target score display | Yes | Calculates and validates |
| Skip Blind button | Shows interaction | Decides whether skip is allowed |
| Tag reward preview | Shows to player | Grants and stores the Tag |
| Temporary effects | Displays effect status | Applies and cleans up effects |
| Game progression | Shows result | Owns rule decisions |
For me, this made one backend principle feel much more concrete:
The backend is not just returning data. It is maintaining the trusted source of game rules.
I had heard similar ideas before.
But building this project made me understand it in a more practical way.
From feature implementation to mechanism design
Looking back, I can roughly divide the project into different levels of difficulty.
| Stage | Main focus | Main difficulty |
|---|---|---|
| Early stage | Cards, hands, scoring, basic actions | Make each action work correctly |
| Blind stage | Blind, Ante, Boss Blind, progression | Decide how state flows across rounds |
| Later stage | Shop, economy, modifiers, special effects | Keep the system extensible |
In the early stage, I mostly asked implementation questions:
How do I detect a poker hand?
How do I shuffle the deck?
How do I remove cards from the hand?
How do I calculate the score?
In the second stage, I started asking design questions:
Who owns this state?
When is this state created?
When should it be destroyed?
Should it survive across Blinds?
Will it need to be persisted later?
Should this become part of a general effect system?
This was a big shift for me.
The code itself did not suddenly become impossible.
But the relationships between states became more important than the code inside one function.
That is why mechanism design felt harder than writing code.
AI can help me code, but it cannot replace project evolution
Another thing I thought about during this process was AI.
Today, tools like ChatGPT, Claude, Cursor, Copilot, and Codex can help developers write code much faster.
They can generate functions. They can review code. They can suggest edge cases. They can explain design patterns. They can even help break down complex problems.
So I asked myself:
If AI can help so much with code, why should I still struggle through this design process myself?
After working on this project for a while, my current answer is:
AI can help me produce code faster, but it cannot replace the experience of watching a project evolve from simple features into a more complex system.
It can suggest how to write a function.
But I still need to decide:
Why this function should exist
Which module owns this state
Whether this rule will expand later
Whether to abstract now or keep it simple
Which boundary must be separated early
Which complexity can wait
These judgments are difficult to learn only by reading tutorials or asking for answers.
They become clearer only after the project keeps growing, and after I repeatedly run into awkward state design problems.
That is also why I still think building a personal project is valuable in the AI era.
Not because AI cannot write code.
But because a long-term project forces me to make engineering decisions.
Writing the article became part of the learning process
There is another unexpected difficulty: writing about the project.
At first, my blog posts were mostly learning notes.
I learned NestJS, so I wrote about NestJS. I added tests, so I wrote about tests. I implemented scoring, so I wrote about scoring.
But now, before writing each article, I need to ask myself:
What problem does this article solve?
Why does this feature belong at this stage?
How is it connected to the previous article?
What foundation does it create for the next feature?
How can I explain the design process clearly?
This is also harder than I expected.
Code can at least run. Tests can pass or fail.
But whether a design explanation is clear is much harder to judge.
Sometimes writing the article forces me to discover that I have not actually understood the design well enough.
And then I have to go back and rethink the code.
That is painful, but useful.
What I learned from getting stuck
This time, I was not stuck because I did not know how to write TypeScript.
I was stuck because I had moved from implementing requirements to defining rules.
That is a very different kind of difficulty.
In a normal business project, many decisions are already made before implementation starts. In this personal backend project, I have to make more of those decisions myself.
There is no perfect answer.
There is only a series of trade-offs:
Simple now or extensible later?
Store state on the Blind or on the player?
Apply an effect immediately or delay it?
Keep a feature hard-coded or introduce an abstraction?
Follow the original game exactly or simplify it for the current backend stage?
This is tiring.
But it also feels like the part that helps me grow the most.
Because real backend work is not only about writing endpoints.
It is also about understanding rules, defining boundaries, managing state, and making sure the system can continue to evolve.
Closing thoughts
This article is not a tutorial and not a final answer.
It is a record of one moment in my project: the moment when a feature that looked simple made me realize that mechanism design can be harder than coding.
The skipBlind feature will eventually become code.
There will be request handlers, services, state updates, tests, and response structures.
But before writing that code, I needed to understand what the feature means inside the system.
That is the part I wanted to record here.
Maybe this is also the real value of this project for me.
It takes me beyond simply receiving requirements and implementing them.
It forces me to ask:
What is the rule? Where does the state live? How does the system move forward? And why should this design exist?
For a backend developer, those questions may be more important than the code itself.
Thanks for reading.
If you are also building a game backend, a personal project, or any system with an evolving state, I would love to hear your thoughts:
How do you decide where a rule should live? And when does a simple feature deserve a real design?
