Logo

Home

About

Blog

Contact

Buy Merch

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Static Members and Utility Classes

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Abstract Classes and Methods
TypeScript
8m
Mar 8, 2026

Abstract Classes and Methods

A comprehensive guide to TypeScript abstract classes covering abstract methods, partial implementation, inheritance patterns, and when to use abstract classes vs interfaces.

#TypeScript Abstract Classes#Abstract Methods+5
Classes in TypeScript: The Basics
TypeScript
7m
Feb 8, 2026

Classes in TypeScript: The Basics

A comprehensive introduction to TypeScript classes covering properties, methods, constructors, and object instantiation with type safety.

#TypeScript Classes#OOP+5
Class Inheritance and the super Keyword
TypeScript
9m
Mar 15, 2026

Class Inheritance and the super Keyword

A comprehensive guide to TypeScript class inheritance covering the extends keyword, super calls, constructor chaining, method overriding, and building effective inheritance hierarchies.

#TypeScript Inheritance#super Keyword+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • Understanding Static Members
  • Static Properties
  • Static Methods
  • Utility Classes
  • The Singleton Pattern
  • Static vs Instance Members
  • Common Patterns and Use Cases
  • Best Practices and When to Avoid
TypeScript

Static Members and Utility Classes

March 1, 2026•15 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript static members and utility classes visualization
Static Members and Utility Classes

Understanding Static Members

Static members belong to the class itself, not to instances of the class. Think of them as class-level properties and methods that exist once, shared across all instances.

When you create instances of a class, each instance gets its own copy of instance properties and methods. But static members? There's only one copy, attached to the class. You access them through the class name, not through instances.

class Counter {
  static count: number = 0; // Static property - shared
  name: string; // Instance property - per instance
 
  constructor(name: string) {
    this.name = name;
    Counter.count++; // Access static via class name
  }
 
  static getCount(): number {
    // Static method
    return Counter.count;
  }
}
 
const c1 = new Counter("First");
const c2 = new Counter("Second");
const c3 = new Counter("Third");
 
console.log(Counter.count); // 3
console.log(Counter.getCount()); // 3
 
console.log(c1.name); // "First" (each instance has own name)
console.log(c2.name); // "Second"
console.log(c3.name); // "Third"
 
// console.log(c1.count); // Error: Property 'count' does not exist on type 'Counter'
// Static members accessed via class, not instances

Every time we create a new Counter instance, the shared count property increments. All instances see the same count because there's only one copy.

Static vs Instance

Static members live on the class. Instance members live on each object created from the class. Use static for shared state or behavior that doesn't depend on instance data.

Static Properties

Static properties store data at the class level. They're initialized once and shared across all instances.

Configuration and Constants

class DatabaseConfig {
  static readonly HOST: string = "localhost";
  static readonly PORT: number = 5432;
  static readonly MAX_CONNECTIONS: number = 100;
  static readonly TIMEOUT_MS: number = 5000;
 
  static getConnectionString(): string {
    return `postgres://${this.HOST}:${this.PORT}`;
  }
}
 
// Access without creating instances
console.log(DatabaseConfig.HOST); // "localhost"
console.log(DatabaseConfig.getConnectionString()); // "postgres://localhost:5432"
 
// No need to instantiate
// const config = new DatabaseConfig(); // Unnecessary!

Using readonly with static creates true constants—values that can't be changed after initialization.

Counters and Registries

class User {
  static totalUsers: number = 0;
  static activeUsers: number = 0;
  static userRegistry: Map<string, User> = new Map();
 
  id: string;
  name: string;
  isActive: boolean = false;
 
  constructor(name: string) {
    this.id = `user_${++User.totalUsers}`;
    this.name = name;
    User.userRegistry.set(this.id, this);
  }
 
  activate(): void {
    if (!this.isActive) {
      this.isActive = true;
      User.activeUsers++;
    }
  }
 
  deactivate(): void {
    if (this.isActive) {
      this.isActive = false;
      User.activeUsers--;
    }
  }
 
  static getStats(): { total: number; active: number; inactive: number } {
    return {
      total: User.totalUsers,
      active: User.activeUsers,
      inactive: User.totalUsers - User.activeUsers,
    };
  }
 
  static findById(id: string): User | undefined {
    return User.userRegistry.get(id);
  }
}
 
const alice = new User("Alice");
const bob = new User("Bob");
const charlie = new User("Charlie");
 
alice.activate();
bob.activate();
 
console.log(User.getStats());
// { total: 3, active: 2, inactive: 1 }
 
const found = User.findById("user_2");
console.log(found?.name); // "Bob"

The registry and counters are shared across all instances. Every user is tracked in the same central registry.

Caching and Memoization

class Fibonacci {
  private static cache: Map<number, number> = new Map([
    [0, 0],
    [1, 1],
  ]);
 
  static calculate(n: number): number {
    if (n < 0) {
      throw new Error("Fibonacci not defined for negative numbers");
    }
 
    // Check cache first
    if (this.cache.has(n)) {
      return this.cache.get(n)!;
    }
 
    // Calculate and cache
    const result = this.calculate(n - 1) + this.calculate(n - 2);
    this.cache.set(n, result);
    return result;
  }
 
  static getCacheSize(): number {
    return this.cache.size;
  }
 
  static clearCache(): void {
    this.cache.clear();
    this.cache.set(0, 0);
    this.cache.set(1, 1);
  }
}
 
console.log(Fibonacci.calculate(10)); // 55
console.log(Fibonacci.calculate(20)); // 6765
console.log(Fibonacci.getCacheSize()); // 21 (cached 0-20)
 
Fibonacci.clearCache();
console.log(Fibonacci.getCacheSize()); // 2 (reset to base cases)

The cache is shared across all calls to calculate(). Computed values persist between calls, making subsequent calculations faster.

Static Methods

Static methods operate at the class level. They can't access instance properties or methods (because there's no instance). They can only access other static members.

Factory Methods

Factory methods are static methods that create instances with preset configurations:

class User {
  constructor(
    public name: string,
    public email: string,
    public role: "admin" | "user" | "guest",
    public verified: boolean = false
  ) {}
 
  // Factory methods
  static createAdmin(name: string, email: string): User {
    return new User(name, email, "admin", true);
  }
 
  static createGuest(): User {
    const guestId = Math.random().toString(36).substring(7);
    return new User(`Guest_${guestId}`, `guest_${guestId}@temp.local`, "guest");
  }
 
  static createFromData(data: {
    name: string;
    email: string;
    role?: "admin" | "user" | "guest";
  }): User {
    return new User(data.name, data.email, data.role || "user");
  }
}
 
// Create users with factory methods
const admin = User.createAdmin("Alice", "[email protected]");
const guest = User.createGuest();
const user = User.createFromData({
  name: "Bob",
  email: "[email protected]",
});
 
console.log(admin); // User { name: 'Alice', role: 'admin', verified: true }
console.log(guest); // User { name: 'Guest_xyz', role: 'guest', verified: false }

Factory methods provide named constructors with clear intent. createAdmin() is more expressive than new User(name, email, "admin", true).

Factory Pattern Benefits

Factory methods give you named constructors, validation before construction, and the ability to return cached instances or subclass instances. They make object creation more flexible and expressive than using new directly.

Validation and Parsing

class Email {
  private constructor(private value: string) {}
 
  static create(email: string): Email | null {
    if (!this.isValid(email)) {
      return null;
    }
    return new Email(email.toLowerCase().trim());
  }
 
  static isValid(email: string): boolean {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  }
 
  static fromDomain(username: string, domain: string): Email | null {
    return this.create(`${username}@${domain}`);
  }
 
  toString(): string {
    return this.value;
  }
 
  getDomain(): string {
    return this.value.split("@")[1];
  }
}
 
const email1 = Email.create("[email protected]");
const email2 = Email.create("invalid-email");
const email3 = Email.fromDomain("bob", "company.com");
 
console.log(email1?.toString()); // "[email protected]"
console.log(email2); // null
console.log(email3?.getDomain()); // "company.com"
 
// Constructor is private - can't bypass validation
// const invalid = new Email("bad"); // Error: Constructor of class 'Email' is private

The private constructor forces all creation through static factory methods, ensuring validation always runs.

Comparison and Utility Methods

class Point {
  constructor(
    public x: number,
    public y: number
  ) {}
 
  static distance(p1: Point, p2: Point): number {
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
 
  static midpoint(p1: Point, p2: Point): Point {
    return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
  }
 
  static origin(): Point {
    return new Point(0, 0);
  }
 
  static fromPolar(radius: number, angle: number): Point {
    return new Point(radius * Math.cos(angle), radius * Math.sin(angle));
  }
}
 
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
 
console.log(Point.distance(p1, p2)); // 5
console.log(Point.midpoint(p1, p2)); // Point { x: 1.5, y: 2 }
console.log(Point.origin()); // Point { x: 0, y: 0 }
 
const p3 = Point.fromPolar(10, Math.PI / 4);
console.log(p3); // Point { x: 7.07..., y: 7.07... }

Static methods work with multiple instances or create instances in specialized ways.

Utility Classes

Utility classes are classes that contain only static members. They're never instantiated—they just group related functionality.

Math Utilities

class MathUtils {
  // Prevent instantiation
  private constructor() {
    throw new Error("MathUtils is a utility class and cannot be instantiated");
  }
 
  static readonly PI_SQUARED = Math.PI * Math.PI;
  static readonly E_SQUARED = Math.E * Math.E;
 
  static clamp(value: number, min: number, max: number): number {
    return Math.max(min, Math.min(max, value));
  }
 
  static lerp(start: number, end: number, t: number): number {
    return start + (end - start) * this.clamp(t, 0, 1);
  }
 
  static randomRange(min: number, max: number): number {
    return min + Math.random() * (max - min);
  }
 
  static randomInt(min: number, max: number): number {
    return Math.floor(this.randomRange(min, max + 1));
  }
 
  static degToRad(degrees: number): number {
    return (degrees * Math.PI) / 180;
  }
 
  static radToDeg(radians: number): number {
    return (radians * 180) / Math.PI;
  }
}
 
console.log(MathUtils.clamp(150, 0, 100)); // 100
console.log(MathUtils.lerp(0, 100, 0.5)); // 50
console.log(MathUtils.randomInt(1, 10)); // Random int between 1-10
console.log(MathUtils.degToRad(90)); // 1.5707... (π/2)
 
// const utils = new MathUtils(); // Error: Cannot instantiate utility class

The private constructor prevents accidental instantiation. This class is purely a namespace for related functions.

String Utilities

class StringUtils {
  private constructor() {}
 
  static capitalize(str: string): string {
    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
  }
 
  static titleCase(str: string): string {
    return str
      .split(" ")
      .map((word) => this.capitalize(word))
      .join(" ");
  }
 
  static truncate(
    str: string,
    maxLength: number,
    suffix: string = "..."
  ): string {
    if (str.length <= maxLength) return str;
    return str.substring(0, maxLength - suffix.length) + suffix;
  }
 
  static slugify(str: string): string {
    return str
      .toLowerCase()
      .trim()
      .replace(/[^\w\s-]/g, "")
      .replace(/[\s_-]+/g, "-")
      .replace(/^-+|-+$/g, "");
  }
 
  static countWords(str: string): number {
    return str.trim().split(/\s+/).length;
  }
 
  static reverse(str: string): string {
    return str.split("").reverse().join("");
  }
}
 
console.log(StringUtils.capitalize("hello world")); // "Hello world"
console.log(StringUtils.titleCase("hello world")); // "Hello World"
console.log(StringUtils.truncate("This is a long sentence", 10)); // "This is..."
console.log(StringUtils.slugify("Hello World! 2024")); // "hello-world-2024"
console.log(StringUtils.countWords("The quick brown fox")); // 4

Array Utilities

class ArrayUtils {
  private constructor() {}
 
  static chunk<T>(array: T[], size: number): T[][] {
    const chunks: T[][] = [];
    for (let i = 0; i < array.length; i += size) {
      chunks.push(array.slice(i, i + size));
    }
    return chunks;
  }
 
  static unique<T>(array: T[]): T[] {
    return [...new Set(array)];
  }
 
  static shuffle<T>(array: T[]): T[] {
    const result = [...array];
    for (let i = result.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [result[i], result[j]] = [result[j], result[i]];
    }
    return result;
  }
 
  static groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
    return array.reduce(
      (groups, item) => {
        const groupKey = String(item[key]);
        if (!groups[groupKey]) {
          groups[groupKey] = [];
        }
        groups[groupKey].push(item);
        return groups;
      },
      {} as Record<string, T[]>
    );
  }
}
 
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(ArrayUtils.chunk(numbers, 3)); // [[1,2,3], [4,5,6], [7,8,9]]
 
const duplicates = [1, 2, 2, 3, 3, 3, 4];
console.log(ArrayUtils.unique(duplicates)); // [1, 2, 3, 4]
 
const users = [
  { name: "Alice", role: "admin" },
  { name: "Bob", role: "user" },
  { name: "Charlie", role: "admin" },
];
console.log(ArrayUtils.groupBy(users, "role"));
// { admin: [{name: 'Alice'...}, {name: 'Charlie'...}], user: [{name: 'Bob'...}] }

Utility Classes vs Modules

Modern TypeScript often prefers exporting functions directly from modules rather than wrapping them in utility classes. Utility classes are a pattern from languages like Java and C#. For new code, consider using module-level functions instead unless you need static state or grouping benefits.

The Singleton Pattern

The singleton pattern ensures a class has only one instance and provides global access to it. Static members make this possible.

Basic Singleton

class Logger {
  private static instance: Logger;
  private logs: string[] = [];
 
  // Private constructor prevents external instantiation
  private constructor() {}
 
  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  log(message: string): void {
    const timestamp = new Date().toISOString();
    this.logs.push(`[${timestamp}] ${message}`);
  }
 
  getLogs(): string[] {
    return [...this.logs];
  }
 
  clearLogs(): void {
    this.logs = [];
  }
}
 
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
 
logger1.log("First message");
logger2.log("Second message");
 
console.log(logger1 === logger2); // true (same instance)
console.log(logger1.getLogs()); // Both messages
console.log(logger2.getLogs()); // Same logs (shared state)
 
// const logger3 = new Logger(); // Error: Constructor is private

Both logger1 and logger2 reference the same instance. All logs are stored in one place.

Application Configuration Singleton

class AppConfig {
  private static instance: AppConfig;
  private settings: Map<string, string> = new Map();
 
  private constructor() {
    // Load default config
    this.settings.set("theme", "dark");
    this.settings.set("language", "en");
    this.settings.set("version", "1.0.0");
  }
 
  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }
 
  get(key: string): string | undefined {
    return this.settings.get(key);
  }
 
  set(key: string, value: string): void {
    this.settings.set(key, value);
  }
 
  getAll(): Record<string, string> {
    return Object.fromEntries(this.settings);
  }
}
 
const config = AppConfig.getInstance();
config.set("theme", "light");
 
// Elsewhere in the app
const sameConfig = AppConfig.getInstance();
console.log(sameConfig.get("theme")); // "light" (shared state)
console.log(sameConfig.getAll());
// { theme: 'light', language: 'en', version: '1.0.0' }

Database Connection Pool Singleton

class ConnectionPool {
  private static instance: ConnectionPool;
  private connections: Array<{ id: number; inUse: boolean }> = [];
  private nextId: number = 1;
 
  private constructor(private maxConnections: number = 10) {
    // Initialize pool
    for (let i = 0; i < maxConnections; i++) {
      this.connections.push({ id: this.nextId++, inUse: false });
    }
  }
 
  static getInstance(maxConnections?: number): ConnectionPool {
    if (!ConnectionPool.instance) {
      ConnectionPool.instance = new ConnectionPool(maxConnections);
    }
    return ConnectionPool.instance;
  }
 
  acquire(): number | null {
    const available = this.connections.find((conn) => !conn.inUse);
    if (available) {
      available.inUse = true;
      return available.id;
    }
    return null; // Pool exhausted
  }
 
  release(id: number): void {
    const conn = this.connections.find((c) => c.id === id);
    if (conn) {
      conn.inUse = false;
    }
  }
 
  getStats(): { total: number; inUse: number; available: number } {
    const inUse = this.connections.filter((c) => c.inUse).length;
    return {
      total: this.connections.length,
      inUse,
      available: this.connections.length - inUse,
    };
  }
}
 
const pool = ConnectionPool.getInstance(5);
 
const conn1 = pool.acquire();
const conn2 = pool.acquire();
 
console.log(pool.getStats()); // { total: 5, inUse: 2, available: 3 }
 
pool.release(conn1!);
console.log(pool.getStats()); // { total: 5, inUse: 1, available: 4 }

class User

Static vs Instance Members

Static Members
Shared across all instances
static totalUsers: number
static getInstance(): User
Access via:
User.memberName

One copy shared by all instances

Accessed via class name

No instance required

Instance Members
Unique to each instance
name: string
email: string
save(): void
Access via:
instance.memberName

Separate copy per instance

Accessed via instance variable

Requires new keyword

Example:
User.totalUsers // Static access
const obj = new User();
obj.name // Instance access

Static vs Instance Members

Understanding when to use static vs instance members is crucial.

When to Use Static

✅ Configuration and constants

class Config {
  static readonly API_URL = "https://api.example.com";
  static readonly TIMEOUT = 5000;
}

✅ Factory methods

class User {
  static createAdmin(name: string) {
    /* ... */
  }
  static createGuest() {
    /* ... */
  }
}

✅ Utility functions that don't need instance data

class StringUtils {
  static capitalize(str: string) {
    /* ... */
  }
}

✅ Shared counters or registries

class Entity {
  static count = 0;
  static registry = new Map();
}

✅ Singleton pattern

class Logger {
  private static instance: Logger;
  static getInstance() {
    /* ... */
  }
}

When to Use Instance

✅ Data that varies per object

class User {
  name: string; // Different for each user
  email: string;
}

✅ Behavior that depends on instance state

class BankAccount {
  balance: number;
  withdraw(amount: number) {
    /* depends on this.balance */
  }
}

✅ When polymorphism is needed

class Animal {
  makeSound() {
    /* overridden in subclasses */
  }
}

Common Patterns and Use Cases

ID Generator

class IdGenerator {
  private static counters: Map<string, number> = new Map();
 
  static generate(prefix: string = "id"): string {
    const current = this.counters.get(prefix) || 0;
    const next = current + 1;
    this.counters.set(prefix, next);
    return `${prefix}_${next}`;
  }
 
  static reset(prefix?: string): void {
    if (prefix) {
      this.counters.delete(prefix);
    } else {
      this.counters.clear();
    }
  }
}
 
console.log(IdGenerator.generate("user")); // "user_1"
console.log(IdGenerator.generate("user")); // "user_2"
console.log(IdGenerator.generate("product")); // "product_1"
console.log(IdGenerator.generate("user")); // "user_3"
 
IdGenerator.reset("user");
console.log(IdGenerator.generate("user")); // "user_1" (reset)

Event Registry

class EventBus {
  private static listeners: Map<string, Array<(...args: any[]) => void>> =
    new Map();
 
  static on(event: string, callback: (...args: any[]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
  }
 
  static emit(event: string, ...args: any[]): void {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      callbacks.forEach((callback) => callback(...args));
    }
  }
 
  static off(event: string, callback?: (...args: any[]) => void): void {
    if (!callback) {
      this.listeners.delete(event);
    } else {
      const callbacks = this.listeners.get(event);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
          callbacks.splice(index, 1);
        }
      }
    }
  }
}
 
EventBus.on("user:login", (user) => console.log(`${user} logged in`));
EventBus.on("user:logout", (user) => console.log(`${user} logged out`));
 
EventBus.emit("user:login", "Alice"); // "Alice logged in"
EventBus.emit("user:logout", "Alice"); // "Alice logged out"

Object Pool

class ObjectPool<T> {
  private static pools: Map<string, ObjectPool<any>> = new Map();
  private available: T[] = [];
  private inUse: Set<T> = new Set();
 
  private constructor(
    private factory: () => T,
    private reset: (obj: T) => void,
    initialSize: number = 10
  ) {
    for (let i = 0; i < initialSize; i++) {
      this.available.push(factory());
    }
  }
 
  static create<T>(
    name: string,
    factory: () => T,
    reset: (obj: T) => void,
    initialSize?: number
  ): ObjectPool<T> {
    if (!this.pools.has(name)) {
      this.pools.set(name, new ObjectPool(factory, reset, initialSize));
    }
    return this.pools.get(name)!;
  }
 
  acquire(): T | null {
    const obj = this.available.pop();
    if (obj) {
      this.inUse.add(obj);
      return obj;
    }
    return null;
  }
 
  release(obj: T): void {
    if (this.inUse.has(obj)) {
      this.inUse.delete(obj);
      this.reset(obj);
      this.available.push(obj);
    }
  }
 
  getStats() {
    return {
      available: this.available.length,
      inUse: this.inUse.size,
    };
  }
}
 
// Usage
const bufferPool = ObjectPool.create(
  "buffers",
  () => new Uint8Array(1024),
  (buffer) => buffer.fill(0),
  5
);
 
const buffer1 = bufferPool.acquire();
const buffer2 = bufferPool.acquire();
 
console.log(bufferPool.getStats()); // { available: 3, inUse: 2 }
 
bufferPool.release(buffer1!);
console.log(bufferPool.getStats()); // { available: 4, inUse: 1 }

Best Practices and When to Avoid

Best Practices

✅ Use readonly for static constants

class Config {
  static readonly MAX_RETRIES = 3; // Can't be changed
}

✅ Prevent instantiation of utility classes

class Utils {
  private constructor() {} // Can't create instances
}

✅ Use factories for complex object creation

class User {
  static createAdmin(name: string) {
    /* validation and setup */
  }
}

✅ Access static members via class name, not this

class Counter {
  static count = 0;
 
  static increment() {
    Counter.count++; // ✅ Clear
    // this.count++; // ❌ Confusing (works but unclear)
  }
}

When to Avoid

❌ Don't use static for data that should be instance-specific

// ❌ Bad: All users share the same name!
class User {
  static name: string;
}
 
// ✅ Good: Each user has their own name
class UserGood {
  name: string;
}

❌ Don't use singletons when you need testability

// ❌ Hard to test (global state)
class Logger {
  private static instance: Logger;
  static getInstance() {
    /* ... */
  }
}
 
// ✅ Better: Dependency injection
class LoggerGood {
  constructor() {}
}

❌ Don't use utility classes when modules suffice

// ❌ Unnecessary class wrapper
class StringUtils {
  static capitalize(s: string) {
    /* ... */
  }
}
 
// ✅ Better: Module-level functions
export function capitalize(s: string) {
  /* ... */
}

Testing Gotcha

Static state persists between tests unless explicitly reset. Always provide reset methods or use dependency injection when testability matters. Global state makes unit tests fragile and order-dependent.

What's Next

You now understand static members and utility classes. In the next posts, you'll learn:

  • Abstract classes and methods for creating blueprints
  • Class inheritance with the super keyword
  • Implementing interfaces in classes
  • Parameter properties shorthand

Key Takeaways

Static members give you:

✅ Class-level functionality that doesn't require instances ✅ Shared state across all instances of a class ✅ Utility classes for grouping related functions ✅ Factory methods for flexible object creation ✅ Singleton pattern for globally shared resources ✅ Constants and configuration in one place

Use static members for shared data, utility functions, and factories. Use instance members for data and behavior that varies per object. When in doubt, start with instance members—they're more flexible and testable. Add static members only when you genuinely need class-level functionality or shared state.