Creating Dynamic Objects with JS Factory Pattern
Learn to create dynamic objects with the JavaScript factory pattern. Covers configuration-driven factories, plugin systems, schema-based generation, prototype chain factories, mixin composition, and builder-factory hybrids for flexible object creation.
Static factories create known types. Dynamic factories create objects whose shape, behavior, and capabilities are determined at runtime by configuration, schemas, or composition. This guide covers advanced factory techniques for building flexible, extensible systems.
For the core factory variants, see The JavaScript Factory Pattern: Complete Guide.
Configuration-Driven Factory
function createFromConfig(config) {
const obj = { type: config.type, id: crypto.randomUUID() };
// Apply properties from config
if (config.properties) {
for (const [key, def] of Object.entries(config.properties)) {
let value = def.default;
// Type coercion
switch (def.type) {
case "string":
value = String(value || "");
break;
case "number":
value = Number(value || 0);
break;
case "boolean":
value = Boolean(value);
break;
case "array":
value = Array.isArray(value) ? [...value] : [];
break;
case "object":
value = value ? { ...value } : {};
break;
}
obj[key] = value;
}
}
// Apply methods from config
if (config.methods) {
for (const [name, fn] of Object.entries(config.methods)) {
obj[name] = fn.bind(obj);
}
}
// Apply validators
if (config.validators) {
obj.validate = function () {
const errors = [];
for (const [field, validator] of Object.entries(config.validators)) {
if (!validator(this[field])) {
errors.push(`Validation failed for "${field}"`);
}
}
return { valid: errors.length === 0, errors };
};
}
return Object.seal(obj);
}
// Define config
const userConfig = {
type: "User",
properties: {
name: { type: "string", default: "" },
email: { type: "string", default: "" },
age: { type: "number", default: 0 },
roles: { type: "array", default: ["viewer"] },
},
methods: {
greet() {
return `Hello, I'm ${this.name}`;
},
hasRole(role) {
return this.roles.includes(role);
},
},
validators: {
name: (v) => v.length >= 2,
email: (v) => v.includes("@"),
age: (v) => v >= 0 && v <= 150,
},
};
const user = createFromConfig(userConfig);
user.name = "Alice";
user.email = "alice@example.com";
user.age = 30;
console.log(user.greet()); // "Hello, I'm Alice"
console.log(user.validate()); // { valid: true, errors: [] }Plugin System Factory
class PluginSystem {
#plugins = new Map();
#hooks = new Map();
registerPlugin(name, plugin) {
if (this.#plugins.has(name)) {
throw new Error(`Plugin "${name}" already registered`);
}
// Validate plugin interface
if (typeof plugin.init !== "function") {
throw new Error(`Plugin "${name}" must have an init() method`);
}
this.#plugins.set(name, {
...plugin,
enabled: true,
initialized: false,
});
return this;
}
registerHook(hookName, pluginName, handler) {
if (!this.#hooks.has(hookName)) {
this.#hooks.set(hookName, []);
}
this.#hooks.get(hookName).push({ pluginName, handler });
return this;
}
async initAll(context = {}) {
for (const [name, plugin] of this.#plugins) {
if (!plugin.initialized) {
await plugin.init(context, this);
plugin.initialized = true;
console.log(`Plugin "${name}" initialized`);
}
}
}
async executeHook(hookName, data) {
const handlers = this.#hooks.get(hookName) || [];
let result = data;
for (const { pluginName, handler } of handlers) {
const plugin = this.#plugins.get(pluginName);
if (plugin?.enabled) {
result = await handler(result);
}
}
return result;
}
createWithPlugins(baseFactory, pluginNames) {
return async (config) => {
let obj = baseFactory(config);
for (const name of pluginNames) {
const plugin = this.#plugins.get(name);
if (plugin?.enhance) {
obj = await plugin.enhance(obj, config);
}
}
return obj;
};
}
getPlugin(name) {
return this.#plugins.get(name);
}
}
// Usage
const system = new PluginSystem();
system.registerPlugin("timestamps", {
init() {},
enhance(obj) {
return {
...obj,
createdAt: Date.now(),
updatedAt: Date.now(),
touch() {
this.updatedAt = Date.now();
},
};
},
});
system.registerPlugin("serializable", {
init() {},
enhance(obj) {
return {
...obj,
toJSON() {
const { toJSON, fromJSON, ...data } = this;
return JSON.stringify(data);
},
fromJSON(json) {
Object.assign(this, JSON.parse(json));
return this;
},
};
},
});
await system.initAll();
const createEnhancedUser = system.createWithPlugins(
(config) => ({ name: config.name, email: config.email }),
["timestamps", "serializable"]
);
const enhancedUser = await createEnhancedUser({
name: "Bob",
email: "bob@example.com",
});
console.log(enhancedUser.createdAt); // timestamp
console.log(enhancedUser.toJSON()); // serializedSchema-Based Object Generator
function createSchemaFactory(schema) {
function generateValue(fieldSchema) {
if (fieldSchema.enum) {
return fieldSchema.enum[0];
}
const defaults = {
string: "",
number: 0,
boolean: false,
array: [],
object: {},
date: () => new Date().toISOString(),
};
if (fieldSchema.default !== undefined) return fieldSchema.default;
const generator = defaults[fieldSchema.type];
return typeof generator === "function" ? generator() : generator;
}
function validateField(value, fieldSchema) {
if (fieldSchema.required && (value === null || value === undefined)) {
return "Field is required";
}
if (fieldSchema.type === "string" && fieldSchema.minLength && value.length < fieldSchema.minLength) {
return `Minimum length: ${fieldSchema.minLength}`;
}
if (fieldSchema.type === "string" && fieldSchema.maxLength && value.length > fieldSchema.maxLength) {
return `Maximum length: ${fieldSchema.maxLength}`;
}
if (fieldSchema.type === "number" && fieldSchema.min !== undefined && value < fieldSchema.min) {
return `Minimum value: ${fieldSchema.min}`;
}
if (fieldSchema.type === "number" && fieldSchema.max !== undefined && value > fieldSchema.max) {
return `Maximum value: ${fieldSchema.max}`;
}
if (fieldSchema.enum && !fieldSchema.enum.includes(value)) {
return `Must be one of: ${fieldSchema.enum.join(", ")}`;
}
if (fieldSchema.pattern && !new RegExp(fieldSchema.pattern).test(value)) {
return `Must match pattern: ${fieldSchema.pattern}`;
}
return null;
}
return {
create(data = {}) {
const instance = {};
for (const [field, fieldSchema] of Object.entries(schema.fields)) {
instance[field] = data[field] !== undefined
? data[field]
: generateValue(fieldSchema);
}
instance.validate = function () {
const errors = {};
for (const [field, fieldSchema] of Object.entries(schema.fields)) {
const error = validateField(this[field], fieldSchema);
if (error) errors[field] = error;
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
};
return instance;
},
createMany(dataArray) {
return dataArray.map((data) => this.create(data));
},
getFieldNames() {
return Object.keys(schema.fields);
},
};
}
// Usage
const productFactory = createSchemaFactory({
fields: {
name: { type: "string", required: true, minLength: 2, maxLength: 100 },
price: { type: "number", required: true, min: 0 },
category: { type: "string", enum: ["electronics", "clothing", "food"] },
inStock: { type: "boolean", default: true },
tags: { type: "array", default: [] },
},
});
const product = productFactory.create({
name: "Wireless Mouse",
price: 29.99,
category: "electronics",
});
console.log(product.validate()); // { valid: true, errors: {} }Mixin Composition Factory
// Mixins as composable behaviors
const Timestamped = (obj) => ({
...obj,
createdAt: Date.now(),
updatedAt: Date.now(),
touch() {
this.updatedAt = Date.now();
return this;
},
});
const Identifiable = (obj) => ({
...obj,
id: crypto.randomUUID(),
});
const Validatable = (rules) => (obj) => ({
...obj,
validate() {
const errors = [];
for (const [field, rule] of Object.entries(rules)) {
if (!rule(this[field])) {
errors.push(`Invalid: ${field}`);
}
}
return { valid: errors.length === 0, errors };
},
});
const Serializable = (obj) => ({
...obj,
toJSON() {
const data = {};
for (const [key, value] of Object.entries(this)) {
if (typeof value !== "function") data[key] = value;
}
return JSON.stringify(data);
},
});
const EventCapable = (obj) => {
const listeners = new Map();
return {
...obj,
on(event, handler) {
if (!listeners.has(event)) listeners.set(event, []);
listeners.get(event).push(handler);
return this;
},
emit(event, ...args) {
(listeners.get(event) || []).forEach((h) => h(...args));
return this;
},
};
};
// Compose factory from mixins
function composeMixins(...mixins) {
return function factory(baseData) {
return mixins.reduce((obj, mixin) => mixin(obj), baseData);
};
}
// Usage
const createTask = composeMixins(
Identifiable,
Timestamped,
Validatable({
title: (v) => v && v.length >= 1,
status: (v) => ["todo", "in-progress", "done"].includes(v),
}),
Serializable,
EventCapable
);
const task = createTask({ title: "Build factory", status: "todo", priority: 1 });
task.on("statusChange", (newStatus) => console.log(`Status: ${newStatus}`));
task.status = "in-progress";
task.touch();
task.emit("statusChange", task.status);
console.log(task.validate()); // { valid: true, errors: [] }
console.log(task.toJSON());Builder-Factory Hybrid
class QueryBuilder {
#table = "";
#conditions = [];
#orderBy = [];
#limit = null;
#offset = null;
#fields = ["*"];
#joins = [];
static for(table) {
const builder = new QueryBuilder();
builder.#table = table;
return builder;
}
select(...fields) {
this.#fields = fields;
return this;
}
where(field, operator, value) {
this.#conditions.push({ field, operator, value });
return this;
}
join(table, on, type = "INNER") {
this.#joins.push({ table, on, type });
return this;
}
orderBy(field, direction = "ASC") {
this.#orderBy.push({ field, direction });
return this;
}
limit(n) {
this.#limit = n;
return this;
}
offset(n) {
this.#offset = n;
return this;
}
build() {
const parts = [`SELECT ${this.#fields.join(", ")}`, `FROM ${this.#table}`];
const params = [];
for (const join of this.#joins) {
parts.push(`${join.type} JOIN ${join.table} ON ${join.on}`);
}
if (this.#conditions.length > 0) {
const whereClauses = this.#conditions.map((c, i) => {
params.push(c.value);
return `${c.field} ${c.operator} $${i + 1}`;
});
parts.push(`WHERE ${whereClauses.join(" AND ")}`);
}
if (this.#orderBy.length > 0) {
parts.push(
`ORDER BY ${this.#orderBy.map((o) => `${o.field} ${o.direction}`).join(", ")}`
);
}
if (this.#limit !== null) parts.push(`LIMIT ${this.#limit}`);
if (this.#offset !== null) parts.push(`OFFSET ${this.#offset}`);
return { sql: parts.join(" "), params };
}
}
// Usage
const query = QueryBuilder.for("users")
.select("id", "name", "email")
.join("orders", "orders.user_id = users.id", "LEFT")
.where("active", "=", true)
.where("age", ">=", 18)
.orderBy("name", "ASC")
.limit(25)
.offset(0)
.build();
console.log(query.sql);
// SELECT id, name, email FROM users LEFT JOIN orders ON orders.user_id = users.id WHERE active = $1 AND age >= $2 ORDER BY name ASC LIMIT 25 OFFSET 0
console.log(query.params); // [true, 18]| Dynamic Factory Type | Object Shape | Runtime Flexibility | Typical Use |
|---|---|---|---|
| Config-driven | From JSON/config | High | CMS, form builders |
| Plugin system | Incrementally enhanced | Very high | Extensible platforms |
| Schema-based | From field definitions | High | API clients, ORMs |
| Mixin composition | Combined behaviors | High | Feature composition |
| Builder-factory | Step-by-step built | Medium | Complex queries, configs |
Rune AI
Key Insights
- Configuration-driven factories create objects from JSON definitions: Property types, default values, methods, and validators can all be specified declaratively
- Plugin systems enhance factory output incrementally: Each plugin adds capabilities to base objects without modifying the core factory
- Schema-based generators validate objects against field definitions: Built-in validation rules enforce constraints at creation time and on demand
- Mixin composition assembles behaviors from independent functions: Composable mixins like Timestamped, Identifiable, and Serializable stack cleanly without inheritance
- Builder-factory hybrids construct complex objects step by step: Fluent APIs guide object construction with validation at the build step
Frequently Asked Questions
How do I make configuration-driven factories type-safe?
When should I use mixin composition over class inheritance?
How do I handle errors in plugin-enhanced factories?
Can dynamic factories be serialized and deserialized?
Conclusion
Dynamic factories create objects whose shape and behavior are determined at runtime. Configuration-driven factories power CMS and form builders. Plugin systems enable extensible platforms. Schema-based generators create validated objects from field definitions. Mixin composition combines independent behaviors. For the foundational factory variants, see The JavaScript Factory Pattern: Complete Guide. For understanding how the observer pattern works with factory-created objects, review JavaScript Observer Pattern: Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.