I am implementing the Builder pattern in a Node.js (TypeScript) backend to instantiate a Permission object before passing it to my repository layer.I want to reuse this same Builder for both creating and updating a permission, but the presence of the id and the validation rules change completely depending on the action:
When Creating: The id attribute is never sent (the database generates it). Both name and module are strictly mandatory.
When Updating: The id is strictly mandatory. However, name and module become optional, with the condition that at least one of them must be provided (they cannot both be missing). If a field is not provided, it should remain null or undefined so the repository knows it shouldn't overwrite that specific column.
import { CustomError } from "../../../shared";
export class PermissionBuilder {
private name: string = '';
private module: string = '';
constructor() {}
setName(name: string): PermissionBuilder {
if (!name || !name.trim()) throw CustomError.badRequest('Name cannot be empty');
this.name = name;
return this;
}
setModule(module: string): PermissionBuilder {
if (!module || !module.trim()) throw CustomError.badRequest('Module cannot be empty');
this.module = module;
return this;
}
build(): { name: string; module: string } {
return {
name: this.name,
module: this.module,
};
}
}
My questions are:
- Is it considered a good practice to handle this conditional logic (where id is missing for creation but required for partial updates) inside a single Builder, or should I split this into two separate builders (e.g., CreatePermissionBuilder and UpdatePermissionBuilder)?
- If keeping a single Builder is the right approach, how should I restructure the internal state and the build() method to handle a payload that dynamically changes its required fields based on whether an id is present or not?
Top comments (1)
Good question, and my honest take: a shared builder is tempting because create and update look similar, but they have genuinely different invariants, and merging them usually leaks complexity. On create, required fields must be present and you're constructing a valid-from-nothing entity; on update, fields are optional/partial, identity is fixed, and some fields shouldn't be mutable at all (created_at, id, ownership). A single builder ends up full of "if updating, skip this validation" branches, which is the smell that you've fused two things that share syntax but not semantics. Separate CreateInput and UpdateInput (or a builder + a patch type) keeps each one's rules honest and lets the type system enforce "you can't set id on create, you can't omit name on create."
The general principle I'd apply: share the genuinely-common bits (field assignment, the underlying mapping) but keep the validation/invariants separate, because that's where create and update actually differ. Conflating distinct rules under one abstraction is the same anti-pattern I watch for everywhere - it's why in Moonshift, the thing I work on (a multi-agent pipeline that takes a prompt to a deployed SaaS), I keep validation explicit per-operation rather than one do-everything path. For most apps: two input types, shared mapping helper. Are your create/update validation rules actually identical, or are you already writing "if id exists" branches? If the latter, that's your answer - split them.