Best Practices/Backend/Architecture/Infrastructure
Best Practices
Shared Column Utility Patterns
Don't
Repeat common columns manually in every table. This leads to:
- Inconsistent column names and types across tables
- Hard-to-apply global changes (e.g., changing timestamp mode)
- Copy–paste errors when adding new tables
DoClick to view
Extract shared columns into a tiny helper module. This provides:
- Consistent column definitions for ids and timestamps
- Single place to change types/defaults for all tables
- Clear intent about “technical” columns vs business columns
// ❌ Bad: Each table redefines common columns by hand// File: infra/database/tables/user.table.tsexport const userTable = pgTable("users", {id: uuid("id").primaryKey().notNull(),createdAt: timestamp("createdAt", { mode: "date" }).notNull().defaultNow(),updatedAt: timestamp("updatedAt", { mode: "date" }).notNull().defaultNow(),email: varchar("email", { length: 320 }),});// File: infra/database/tables/document.table.tsexport const documentTable = pgTable("documents", {id: uuid("id").primaryKey().notNull(), // duplicatedcreatedAt: timestamp("createdAt", { mode: "date" }).notNull().defaultNow(), // duplicatedupdatedAt: timestamp("updatedAt", { mode: "date" }).notNull().defaultNow(), // duplicatedtitle: varchar("title", { length: 255 }),});
Serialization & Flattening Patterns for nested objects
Don't
Manually map nested objects inline in each repository method. This causes:
- Repeated, slightly different mapping logic in multiple places
- Easy-to-miss fields when the domain model changes
- Hidden coupling between DB shapes and domain shapes
DoClick to view
Use a single serialization / deserialization path (e.g. Effect Schema) to flatten and rebuild entities. This provides:
- Centralized mapping between domain entity ↔ serialized shape ↔ DB row
- Safer nested handling for value objects and aggregates
- Reusable helpers in the repository instead of ad-hoc mapping
// ❌ Bad: Manual, scattered mapping logic in each repository methodconst host = /* Host entity from domain */;// Repository manually flattens nested objects into columnsconst row = {id: host.id,firstName: host.firstName,lastName: host.lastName,// address flattened by handcountry: host.address.country,city: host.address.city,// ... manually map each address field// socialLinks flattened by handwebsiteLink: host.socialLinks.websiteLink,// ... manually map each social link};
Query Helper Utility Patterns
Don't
Inline query execution and mapping in every repository method. This causes:
- Duplicated error handling and logging
- Slightly different behaviors for “single vs multiple” queries
- Harder to introduce cross-cutting concerns (timeouts, metrics, etc.)
DoClick to view
Centralize query execution and mapping in small helpers. This provides:
- Single place to handle driver errors and wrap them in repository errors
- Consistent behavior for “single” and “multiple” fetch patterns
- Cleaner repository methods focused only on what they query
// ❌ Bad: Each method does its own query + error + mappingfetchByUserId(userId: UUID): RepositoryEffect<O.Option<Host>, HostNotFoundError> {return E.tryPromise({try: () =>this.db.select().from(hosts).where(eq(hosts.userId, userId)).limit(1),catch: () => new HostNotFoundError(),}).pipe(E.flatMap((rows) => {if (rows.length === 0) {return E.succeed(O.none());}// Inline DB row -> Host mappingconst row = rows[0];const serializedHost = {...row,address: {country: row.country,city: row.city,// ... manually reconstruct each address field},socialLinks: {websiteLink: row.websiteLink,// ... manually reconstruct each social link},};return Host.create(serializedHost).pipe(E.map(O.some),E.mapError(() => new HostNotFoundError()),);}),);}