The Case for Factory Functions: Reducing Bugs and Technical Debt in Angular + Microservices Projects
In modern Angular and microservices-based applications, managing data consistency and reliability is a persistent challenge. When developing the frontend with Angular/React, we are often at the mercy of backend API changes, partial data responses, and unpredictable user input. Despite advances in TypeScript’s strong typing, many teams still suffer from runtime errors like “Cannot read property ‘x’ of undefined”, broken UI states, and backend validation failures due to improperly formed objects. In short, manual object construction invites small, drift(*), duplication, and bugs into codebases; problems that compound as the application and team grow.
A recurring pattern emerges: manual object construction, where developers instantiate types inline by explicitly setting each property. While this approach seems fast initially, it introduces hidden risks as applications grow in complexity.
This article explores why using factory functions with Partial types is a superior approach, especially in Angular projects that consume multiple microservices. Factory functions not only eliminate undefined fields and runtime crashes but also significantly improve maintainability, testability, and long-term team velocity.
By centralizing both the type definition and its factory function in a single place, we create a clear source of truth for each data structure. This proactive strategy not only guarantees consistent object shapes across the application but also dramatically simplifies maintenance when backend contracts evolve. By enforcing sensible defaults at creation time, we reduce bugs, simplify testing, and future-proof our applications against schema changes and service instability. In the sections that follow, we will break down the risks of manual object creation, the core benefits of factory functions, real-world examples where defaults saved production systems, and how a small upfront investment in a clean factory pattern pays off exponentially as systems scale.
The Risks of Manual Object Construction
In large-scale applications, especially those built atop multiple backend microservices, manual object construction quickly becomes a source of hidden technical debt. When developers create objects inline by specifying each property manually, several risks emerge: Let’s take a look at an initial concept when you manually create your Object:
const userProfile: UserProfile = { id: 'abc123', name: 'John Doe' };
//{ id: 'abc123', name: 'John Doe' } <-- this may be a response from an API call,
//or you manually want to set it this way for whatever reason
Now let’s suppose the UserProfile has more fields:
const userProfile: UserProfile = {
id: 'abc123',
name: 'John Doe',
email: '[email protected]',
isPremiumUser: true,
lastLogin: new Date(),
//...potentially many more fields
};
But we want to set id and name only; It’s cleaner, looks nicer, or you’re just in a hurry; you might be tempted to modify the type like this:
type UserProfile = {
id: string;
name: string;
email?: string;
isPremiumUser?: boolean;
lastLogin?: Date;
};
Some reasons why this is probably a bad idea:
- The data model is now not really dependable. In reality, fields like email and lastLogin might be required at runtime.
- You lose type safety because now, everywhere in your app, TypeScript will think email might be missing even in places where it must exist.
- You’re inviting bugs, you probably don’t need anymore do you? ; so why take a chance, you also have to constantly write guards like -→ if (userProfile.email) :even in places where it should never be missing.
- You weaken backend communication because when you send payloads back to an API, fields may be incorrectly omitted.
Instead of weakening your types, a better solution is to keep the type strict and use a factory function with Partial
export function createUserProfile(init: Partial = {}): UserProfile {
return {
id: init.id ?? '',
name: init.name ?? '',
email: init.email ?? '',
isPremiumUser: init.isPremiumUser ?? false,
lastLogin: init.lastLogin ?? new Date(0),
};
}
This method preserves the full type safety of UserProfile throughout the application, while still allowing partial initialization during object creation.
const userProfile = createUserProfile({ id: 'abc123', name: 'John Doe' });
Some Issues You Can Expect With Manual Object Creation
Missing or Undefined Properties – Backend services evolve. New fields are added, and existing ones change. Manually instantiated objects often leave out new or updated fields, … could lead to runtime errors like undefined, is not a function or broken UI elements. These bugs are difficult to detect at compile-time and often only surface during late-stage testing or worse, in production.
Fragile, Duplicated Code – Without centralized object creation, the same object structure must be manually recreated across components, services, and tests. This duplication increases the chance of inconsistency. If a new required field is added, every manual instantiation must be found and updated individually, a tedious and error-prone process that makes your day bad.
Harder Testing and Debugging – Tests often rely on mock objects. If defaults aren’t enforced systematically, test mocks may differ subtly from production data shapes. This leads to false positives or missed edge cases during testing, undermining the value of your test suite. You might think it’s not big deal now, but your application might grow into an extremely complicated piece of software other people have to start working on.
Unreliable Data Sent to Backends – In financial or commercial applications, sending incomplete or malformed data to backend services can have serious consequences. Manual object creation increases the risk of sending null, undefined, or otherwise invalid payloads that cause backend validation errors or, worse, silent data corruption(SDC).
(*) Over time, as manual object construction continues, your app’s models, data contracts, and backend expectations gradually “drift apart,” creating inconsistencies that are hard to detect but costly 🪲 to fix.