Social Simulation · Architecture
📅 July 2026 ⏱ ≈ 11 min read 🎯 Intermediate

Agent-Based Modelling: Architecture & Design Patterns

Every crowd evacuation, market bubble, and segregation pattern on this site is built from the same skeleton: a population of independent agents, a perceive-decide-act loop, and a scheduler that ties it all together. This article breaks down that skeleton so you can design your own agent-based model (ABM) from scratch.

TL;DR: An agent-based model is built from three reusable parts: per-agent state, a perceive-decide-act loop that keeps updates order-independent, and a spatial index (grid or quadtree) so neighbour lookups stay fast at thousands of agents. This article shows how those parts combine, plus a minimal random-scheduling JavaScript skeleton, to produce emergent patterns like segregation or evacuation bottlenecks from simple local rules.

What is agent-based modelling?

An agent-based model replaces a single global equation with a population of autonomous agents, each following simple local rules and reacting only to their immediate neighbourhood. Instead of solving "what is the average behaviour of the system", you simulate every individual and let the aggregate pattern emerge from the bottom up. This is the opposite approach to a differential-equation model like SIR or Lotka-Volterra, which describes populations as continuous quantities.

ABM is the natural tool whenever heterogeneity, local interaction, or discrete decisions matter: pedestrians choosing an exit, households choosing a neighbourhood, traders choosing to buy or sell. This site's crowd evacuation, Schelling segregation, and Prisoner's Dilemma simulations are all ABMs built on the same architecture described below.

Agent state and behaviour

Every agent needs a compact state — the minimum data required to decide its next action — and a behaviour function that maps state plus local observations to a new state. Keeping state minimal matters for performance: with 10,000+ agents updating every frame, every extra field is 10,000 extra bytes moved through the CPU cache each tick.

Field categoryExamplesMutable per tick?
Kinematicposition, velocity, headingyes
Internalgoal, mood, wealth, opinionyes
Identity / typespecies, strategy, grouprarely
Derived (cached)neighbour list, local densityyes, recomputed

A common mistake is entangling behaviour logic with rendering code. Keep agents as plain data (Structure-of-Arrays or a lightweight class) and treat drawing as a separate pass that only reads state — this makes the model trivially portable between Canvas 2D, WebGL and headless batch runs used for parameter sweeps.

The perceive-decide-act loop

Nearly every agent-based model, regardless of domain, factors its per-tick update into three phases:

1. PERCEIVE — gather local information (neighbours, environment cells, prices)
2. DECIDE — apply the behaviour rule to produce an intended action
3. ACT — apply the action to update state (move, trade, switch strategy)

Splitting decide from act is not a stylistic choice — it is what makes the simulation order-independent. If agent A acts immediately after deciding, agent B (processed next in the same tick) perceives A's new position instead of the position it had at the start of the tick. That silently changes the dynamics and makes results depend on iteration order, which is rarely intended.

Rule of thumb: compute all decisions first into a buffer, then apply all actions in a second pass. This "double buffering" pattern costs one extra array per field but guarantees deterministic, order-independent updates — essential for reproducible experiments and for parallelising the decide phase across web workers.

Spatial indexing: grids, quadtrees, spatial hashing

The perceive phase usually needs "who is near me", and a naive O(N²) all-pairs scan becomes the bottleneck past a few thousand agents. Three structures cover almost every ABM use case:

StructureBest forQuery cost
Uniform gridroughly uniform density, fixed interaction radiusO(1) avg
Spatial hash mapunbounded world, sparse agentsO(1) avg
Quadtree / k-d treehighly uneven density (cities, clusters)O(log N)

For a crowd evacuation or the Boltzmann-Pareto market simulation, a uniform grid sized to roughly the interaction radius is almost always the right first choice: rebuild it every tick in O(N), then each neighbour query only scans the 3×3 (or 9-cell) block around the agent instead of the whole population.

class SpatialGrid {
  constructor(cellSize) {
    this.cellSize = cellSize;
    this.cells = new Map();  // key: "cx,cy" -> array of agent indices
  }
  key(x, y) {
    const cx = Math.floor(x / this.cellSize);
    const cy = Math.floor(y / this.cellSize);
    return cx + ',' + cy;
  }
  rebuild(agents) {
    this.cells.clear();
    for (let i = 0; i < agents.length; i++) {
      const k = this.key(agents[i].x, agents[i].y);
      if (!this.cells.has(k)) this.cells.set(k, []);
      this.cells.get(k).push(i);
    }
  }
  neighbours(x, y, agents) {
    const cx = Math.floor(x / this.cellSize);
    const cy = Math.floor(y / this.cellSize);
    const out = [];
    for (let dx = -1; dx <= 1; dx++)
      for (let dy = -1; dy <= 1; dy++) {
        const bucket = this.cells.get((cx+dx) + ',' + (cy+dy));
        if (bucket) out.push(...bucket);
      }
    return out;
  }
}

Scheduling order: synchronous vs asynchronous

Beyond decide/act separation, you must choose who updates when:

Common bug: shuffling agent order once at initialization instead of every tick. This silently reintroduces order bias — agent 37 in the shuffled array still always updates right after agent 12, tick after tick.

Where emergence comes from

Emergence is the appearance of system-level structure — segregated neighbourhoods, flocking, price bubbles, evacuation bottlenecks — that is not explicitly coded into any single agent's rule. It arises from three ingredients working together:

  1. Local interaction: agents respond only to nearby agents/cells, never to a global average.
  2. Nonlinearity or thresholds: a small change in a local variable (one more unlike neighbour, one more panicked pedestrian) can flip an agent's decision entirely.
  3. Feedback: an agent's action changes the local environment that its neighbours perceive next tick, which changes their actions, which changes the environment again.

This is precisely why Thomas Schelling's segregation model is so striking: no agent in the model has a strong preference for total segregation — a threshold of "just 30% similar neighbours" is enough — yet the emergent city-scale pattern is near-complete separation. The lesson generalises: to understand a complex social pattern, look for the simple local rule and the feedback loop that amplifies it, not a single "cause" at the system level.

A minimal ABM skeleton in JavaScript

Putting the previous sections together, here is the shape every ABM on this site follows — a spatial index, a decide/act split, and randomised scheduling:

class Simulation {
  constructor(n, worldSize) {
    this.agents = Array.from({ length: n }, () => ({
      x: Math.random() * worldSize,
      y: Math.random() * worldSize,
      vx: 0, vy: 0,
      state: 'idle',
    }));
    this.grid = new SpatialGrid(10);
  }

  step(dt) {
    this.grid.rebuild(this.agents);
    const order = this.agents.map((_, i) => i);
    shuffle(order); // random asynchronous scheduling

    // 1) DECIDE — compute intents without mutating state yet
    const intents = new Array(this.agents.length);
    for (const i of order) {
      const a = this.agents[i];
      const nearby = this.grid.neighbours(a.x, a.y, this.agents);
      intents[i] = decide(a, nearby, this.agents);
    }
    // 2) ACT — apply all intents
    for (const i of order) {
      act(this.agents[i], intents[i], dt);
    }
  }
}

Everything else — the Social Force Model for evacuation, payoff-matrix strategy updates for the Prisoner's Dilemma, or Schelling's similarity threshold — plugs into decide() and act(). The scaffolding around it (grid, scheduling, double buffering) stays the same across radically different domains.

👥 Run the crowd evacuation ABM

Social Force Model, spatial grid, hundreds of agents finding the exit in real time

Open simulation →