There is a particular category of technical regret that doesn't announce itself until it's too late to be cheap.
It's not the bug you introduced last sprint. Bugs are visible, traceable, fixable. It's not the feature you built that nobody used — that's a product mistake, and product mistakes are recoverable. The category I'm describing is structural. It's the decision made in week two of a project that looked completely reasonable at the time, that nobody flagged in review because it seemed fine, and that six months later is the reason a change that should take an afternoon is taking a week.
Every engineer who has worked on a backend long enough has a version of this story. In Serverpod specifically — where the framework's code generation means your early structural decisions ripple outward into generated code, database schema, and client interfaces simultaneously — those decisions have a wider blast radius than they do in most other frameworks.
This article is about the ones that come up most often. Not as a warning to freeze up before making decisions, but as a map of the territory where the ground is soft — so you can move through it with your eyes open.
The Model Design Decisions That Compound
Your Serverpod models are the foundation of everything. They define your database schema, your serialisation, your client-side types, and the shape of data that moves through every layer of your application. Getting them wrong early doesn't just mean fixing a class — it means regenerating code, writing migrations, updating every endpoint that touches that model, and updating the Flutter code that consumes it.
The decisions that cause the most pain later are rarely dramatic. They're small, reasonable-seeming choices made when you didn't yet have the full picture of how the data would be used.
Naming that doesn't survive growth. A model named User works fine until you need to distinguish between app users, admin users, and guest sessions. A field named status works fine until you have five different things that could reasonably be called status. Names that are accurate for the first version of a feature often become ambiguous as the feature grows, and in Serverpod, renaming a model or field means a migration, a regeneration, and a ripple through every call site. The investment in names that will still be accurate in a year pays for itself quickly.
Flat models that should have been relational from the start. The temptation to embed related data directly in a model — storing a list of tags as a comma-separated string, keeping an address as a single text field, representing a relationship as a stored ID with no database-level enforcement — is strongest when you're moving fast and the relationship seems simple. It stops feeling simple when you need to query by tag, validate an address field, or enforce referential integrity across a relationship that has now accumulated real data. Relational data that starts flat rarely migrates cleanly, and the migration always happens at the worst possible time.
Missing timestamps. createdAt and updatedAt fields feel unnecessary until the moment a stakeholder asks "when did this record change?" or a bug report arrives with no temporal context. Adding them retroactively means a migration that sets a value for every existing row — a value that is, at best, an approximation. Adding them from the start costs two lines of YAML.
Overloading a single model. A model that starts as a representation of one concept and gradually accumulates fields for adjacent concepts is one of the most common sources of structural debt in any backend. In Serverpod, it's particularly visible because the generated class becomes large, the database table develops nullable columns that only apply in certain contexts, and the endpoint logic starts branching on which kind of thing this model actually represents right now. When a model starts needing a type field to tell your code what it actually is, it's usually a sign that two or three models were collapsed into one under time pressure.
The Endpoint Structure Decisions That Drift
Endpoints in Serverpod are where your server's behaviour lives, and the structural decisions you make about them early tend to propagate across your entire API as new endpoints are added following the same patterns — for better or worse.
Authentication checks applied inconsistently. Serverpod gives you the session, and the session gives you access to authentication state. How you check that authentication — whether you verify it at the start of every method, whether you build a helper that centralises the check, whether certain endpoints are explicitly public or private by default — is a decision you make implicitly every time you write a new method. If that decision isn't made explicitly and early, you end up with an API where some methods check auth, some assume it's been checked elsewhere, and the boundary between protected and unprotected behaviour is enforced by convention rather than structure.
Error handling that evolved without a plan. What does your API return when something goes wrong? A Serverpod endpoint can throw an exception, return null, return an empty list, or return a result object that wraps either a value or an error. All of these are valid patterns. The problem is when all of them appear in the same codebase, applied inconsistently across endpoints, because each one was written by whoever was working on it that day following whatever felt right at the time. A Flutter client consuming an API with inconsistent error shapes has to handle each endpoint differently, and the defensive code that accumulates on the client side to handle all the possible failure modes becomes its own maintenance burden.
Endpoints that know too much. An endpoint that reaches directly into the database, applies business rules inline, calls external services, and formats the response in the same method is doing too many things. It's also impossible to test in isolation and difficult to change without understanding all of its dependencies at once. The instinct to keep things simple by putting everything in one place is understandable early in a project. It becomes a trap when the method that started at fifteen lines grows to a hundred and fifty, and the logic that needs to change is buried in the middle of it.
The discipline of separating endpoint logic — the method that receives a request — from service logic — the code that does the actual work — pays dividends that compound as a project grows. The endpoint stays thin. The service is independently testable. The boundary between "receiving a request" and "doing something about it" stays clear.
The Database Decisions That Lock You In
Postgres, which sits under Serverpod, is a capable and flexible database. It will accommodate a lot of mistakes. But some decisions made early in a project's database layer have a way of becoming load-bearing before anyone realises they need to be revisited.
No indexes, until there are. A database table with no indexes performs perfectly well on a dataset of a few hundred rows. It performs noticeably differently on a dataset of a few hundred thousand. The columns you query most often, the foreign keys you join on, the fields you order results by — these are candidates for indexes from the moment you know they'll be used that way. Adding indexes to a table with real data is not catastrophic, but it requires a migration, and on large tables it can be slow enough to require a maintenance window. The developer who thinks about indexes when writing the model definition rather than when diagnosing a slow query is working with better information.
Migrations that were never reviewed. Serverpod generates migrations automatically, and the generated output is usually correct. Usually. A migration that drops a column because you removed it from your model, a migration that changes a column type in a way that truncates existing data, a migration that creates a table with a constraint that existing data would violate — these are all things that a generated migration can do correctly from the framework's perspective, and catastrophically from the data perspective. The habit of reading a generated migration before running it, every time, is one of the highest-value habits in Serverpod development. It takes two minutes. The alternative can take considerably longer.
Treating the database as a detail. The view that the database is just a persistence layer — a place where your models get stored and retrieved, not a part of the architecture that deserves its own attention — leads to backends that are structurally correct at the Dart level and fragile at the data level. Constraints, relationships, data types, and nullability exist in the database for reasons, and those reasons are not made redundant by having them in your Dart models as well. A database that enforces its own integrity is one that remains correct even when application code behaves unexpectedly.
The Decision Nobody Thinks Is a Decision
There is one more architecture decision in Serverpod development that rarely gets recognised as a decision at all, and it might be the most consequential.
It's the decision about how much of the framework's workflow to automate, and how much to leave as a manual process.
Every Serverpod project has a version of the same sequence: change a model, run generate, check the output, create a migration, review the migration, apply it, verify the result. That sequence is correct and necessary. What varies between projects — and between developers — is whether it happens reliably, consistently, and with enough visibility that mistakes are caught before they reach a database with real data in it.
Projects that treat this workflow as a manual process that each developer runs when they remember to, without any shared visibility into what's been generated or what migrations are pending, are the projects that accumulate the kinds of structural problems described in this article. Not because the developers are careless, but because the workflow creates conditions where carelessness is easy and visibility is low.
The projects that develop explicit habits around this workflow — reviewing generated output before committing it, treating migration files as first-class code that deserves careful inspection, having a shared understanding of what the current database schema actually looks like — are the ones where the architecture stays clean over time.
The tools and habits that make that workflow more visible, more reliable, and harder to get wrong are not a luxury. They're the difference between a backend that stays coherent as it grows and one that accumulates the kind of structural debt that eventually rewrites itself on your behalf, at a time and cost of its choosing.
Architecture is mostly made of small decisions. The ones that haunt you are the ones made without realising they were decisions at all.
