Asher Cohen
Back to posts

Data Modeling and State Management

Understanding state, entities, normalization, serialization, and data modeling patterns in JavaScript applications

Introduction

How you model and manage data determines the architecture of your entire application. Understanding state, entities, normalization, and serialization helps you build applications that are predictable, performant, and maintainable.

What is State?

State is data that changes over time and determines what your application displays and how it behaves.

// Application state examples
const appState = {
  // UI state
  theme: 'dark',
  sidebarOpen: false,
  activeModal: null,
  
  // Domain state
  currentUser: { id: 1, name: 'Alice' },
  cart: { items: [], total: 0 },
  
  // Server state (cached)
  users: [],
  posts: [],
  
  // Communication state
  loadingStates: {},
  errors: {}
};

Types of state:

  • UI state — Modals, themes, sidebar visibility
  • Domain state — Business data (users, orders, products)
  • Server state — Cached API responses
  • Communication state — Loading flags, error messages

Entities

Entities are objects with a unique identity that persists over time:

// Entity definition
interface User {
  id: string;        // Unique identifier
  name: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

// Entity identity
const user1 = { id: '1', name: 'Alice' };
const user2 = { id: '1', name: 'Alice Updated' };
// Same entity (same id), different state

const user3 = { id: '2', name: 'Alice' };
// Different entity (different id), even though name matches

Entity characteristics:

  • Has a unique identifier
  • Mutable over time (state changes, identity stays)
  • Can be compared by ID, not by value
  • Often corresponds to database rows

Normalization

Store data in a flat, relational structure rather than nested:

// ❌ Nested — hard to update, duplicates data
const nestedState = {
  posts: [
    {
      id: 1,
      title: 'Post 1',
      author: { id: 1, name: 'Alice', avatar: 'alice.jpg' },
      comments: [
        { id: 1, text: 'Great!', author: { id: 2, name: 'Bob' } }
      ]
    }
  ]
};

// ✅ Normalized — single source of truth, easy updates
const normalizedState = {
  posts: {
    byId: {
      1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] }
    },
    allIds: [1]
  },
  users: {
    byId: {
      1: { id: 1, name: 'Alice', avatar: 'alice.jpg' },
      2: { id: 2, name: 'Bob', avatar: 'bob.jpg' }
    },
    allIds: [1, 2]
  },
  comments: {
    byId: {
      1: { id: 1, text: 'Great!', authorId: 2, postId: 1 }
    },
    allIds: [1]
  }
};

Benefits of normalization:

  • ✅ Single source of truth — update a user once, reflected everywhere
  • ✅ No data duplication
  • ✅ Easier updates — modify flat objects
  • ✅ Better performance — avoid deep cloning

Serialization

Converting data between in-memory objects and transferable formats:

// Serialize: object → JSON string
const user = { id: 1, name: 'Alice', createdAt: new Date() };
const json = JSON.stringify(user);
// '{"id":1,"name":"Alice","createdAt":"2024-01-01T00:00:00.000Z"}'

// Deserialize: JSON string → object
const parsed = JSON.parse(json);
// Note: createdAt is now a string, not a Date!

// Custom serialization with toJSON()
class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
    this.createdAt = new Date();
  }
  
  toJSON() {
    return {
      id: this.id,
      name: this.name,
      createdAt: this.createdAt.toISOString()
    };
  }
}

Serialization considerations:

  • Dates become strings — need reviver to restore
  • Functions, undefined, symbols are lost
  • Maps and Sets become empty objects
  • BigInt throws TypeError
  • Circular references throw TypeError

Data Modeling

Relational Modeling

// One-to-many: User → Posts
const user = { id: 1, name: 'Alice' };
const posts = [
  { id: 1, userId: 1, title: 'Post 1' },
  { id: 2, userId: 1, title: 'Post 2' }
];

// Many-to-many: Posts ↔ Tags
const postTags = [
  { postId: 1, tagId: 1 },
  { postId: 1, tagId: 2 },
  { postId: 2, tagId: 1 }
];

Domain-Driven Modeling

// Model real-world concepts as entities and value objects
class Order {
  constructor(id, customerId) {
    this.id = id;              // Entity (has identity)
    this.customerId = customerId;
    this.items = [];           // Value objects
    this.status = 'pending';   // State
  }
  
  addItem(product, quantity) {
    this.items.push({ product, quantity });
  }
  
  submit() {
    if (this.items.length === 0) throw new Error('Empty order');
    this.status = 'submitted';
  }
}

Event Sourcing

// Store events, not current state
const events = [
  { type: 'OrderCreated', orderId: 1, customerId: 1 },
  { type: 'ItemAdded', orderId: 1, product: 'Book', quantity: 2 },
  { type: 'ItemAdded', orderId: 1, product: 'Pen', quantity: 1 },
  { type: 'OrderSubmitted', orderId: 1 }
];

// Rebuild current state from events
function rebuildState(events) {
  return events.reduce((state, event) => {
    switch (event.type) {
      case 'OrderCreated':
        return { id: event.orderId, items: [], status: 'draft' };
      case 'ItemAdded':
        return { ...state, items: [...state.items, event] };
      case 'OrderSubmitted':
        return { ...state, status: 'submitted' };
      default:
        return state;
    }
  }, null);
}

Best Practices

Do's

// ✅ Normalize relational data
// ✅ Use unique IDs for entities
// ✅ Separate UI state from domain state
// ✅ Serialize dates explicitly
// ✅ Model data before coding
// ✅ Use TypeScript interfaces for data shapes

Don'ts

// ❌ Don't deeply nest related data
// ❌ Don't store derived values (calculate on read)
// ❌ Don't mix concerns (UI state in domain models)
// ❌ Don't serialize functions or class instances
// ❌ Don't use arrays for entity lookups (use objects/maps)

Summary Table

ConceptPurposePattern
StateData that changes over timeSeparate UI/domain/server state
EntitiesObjects with identityUnique ID, mutable properties
NormalizationFlat data structurebyId + allIds pattern
SerializationData transfer formatJSON with custom toJSON()
ModelingData structure designRelational, DDD, event sourcing

Conclusion

Good data modeling is the foundation of good software:

  • Understand your state — What changes? Where does it live?
  • Normalize early — Flat structures are easier to work with
  • Model entities carefully — Identity matters more than values
  • Serialize thoughtfully — JSON has limitations you must handle

Invest time in data modeling before writing code, and your application architecture will be stronger for it.

#data-modeling #state-management #javascript #normalization #architecture #web-development