Always add non-null db constraints (unless completely infeasible)Non-null constraints ensure that critical fields are never left empty, improving data integrity and preventing unexpected errors. For example, a nullable boolean introduces a third state, so always enforce non-null constraints at the database level when possible.
Think of ON DELETE policiesMake sure to make a conscious decision about when and how your records should be deleted if the associated records are deleted. For example, if you have a user, and a user has many posts, what should happen to the posts when the user is deleted? Should they be deleted as well? Should they be orphaned? Should they be reassigned to another user?
Make sure time is always UTCAlways store time in UTC. When fetching the timestamp for comparison, it will be fetched in the server's timezone, not UTC, so be aware of that.
Make sure dates and times are always of the same data typePick the most suitable type and use it everywhere. And by everywhere, I mean everywhere. I personally prefer using timestamp
for everything.
Do not prematurely create indexes (unless absolutely necessary)Due to performance, storage, and complexity overheads, adding an index should always be a deliberate and well-considered decision. I prefer adding an index only when querying by that column becomes a bottleneck.
Use timestamps instead of (or alongside) status flagsImagine a post that can be in 3 states: draft, published, archived. Instead of using a status flag, use timestamps for each state, and an XOR constraint to ensure only one of them is set at a time.
id, title, content, draft_at, published_at, archived_at
You can also add a status flag for convenience, with the necessary integrity constraints added to it as well.
Mirror validations in databaseWhen using Ecto's unique_constraint
or validate_*****
in changesets, ensure that the same constraints exist in the database. For bulk updates or inserts, changeset-level validations aren't applied, so always begin with database-level validations and either handle errors gracefully with unique_constraint
or duplicate them using validate_****
inside the application.
Avoid using Ecto's embedded schemas for data managed by the application (opinionated)Use embedded schemas only when storing raw data whose schema isn’t controlled by your application. For example, a correctly used embedded schema might represent a response from a 3rd party API. Embedded schemas are not typed by Postgres, and default values are stored in a schema struct, so when filtering on the DB level, they're not applied. This can lead to hard-to-catch inconsistencies, so it’s better to omit embeds when possible.
Additionally, it's hard to create constraints on a JSONB column, so if possible, don't use it.