Algorithm code is compact because it removes the history of execution. A two-pointer loop may be ten lines, but understanding it requires tracking pointer positions, maxima, accumulated output, and the unresolved region at every iteration. The source shows the rule; a trace shows why the rule is safe now.

Visualization is most useful when it maps directly to state rather than decorating the result. Trapping rain water needs two boundaries and changing maxima. Top K frequency needs the data transformation from values to counts to buckets. Selection sort needs a visible sorted boundary, scan index, candidate minimum, and swap event.

A good algorithm visualizer is therefore an execution system. It captures deterministic steps, serializes variable changes, supports forward and backward movement, and chooses a view that matches each data structure. The same architecture helps teaching, debugging, regression analysis, and explaining production decisions whose correctness depends on a sequence rather than a final value.

Visual walkthrough / three mental models

Let the changing state do the explaining.

Do not begin by memorizing code. Begin by naming what is known, what is still unknown, and which value changes next. Once those three things are visible, the implementation becomes a transcription of the reasoning.

01 / Two pointers

Trapping Rain Water

Question: how much water remains between walls after rain falls?

20
201
112
33

At index 1, the ground height is 0. The best wall seen from the left is 2, and the wall on the right is 3. Water can rise only to the shorter boundary, so this position holds min(2, 3) - 0 = 2 units. Index 2 has height 1 and holds one more unit. The answer is 3.

Why move the shorter side?

Suppose the left wall is shorter than the right wall. The right side is already tall enough to stop water escaping from the current left position. A taller wall somewhere farther right cannot change the fact that the left side is the limiting boundary. That means the current left position can be solved now. We update leftMax, add any water above the current height, and move left inward. The same argument works in reverse when the right wall is shorter.

StepLeftRightLeft maxRight maxWaterDecision
10 → 23 → 3200Solve left
21 → 03 → 3202Add 2
32 → 13 → 3203Add 1
43 → 33 → 3303Finished
function trap(height: number[]): number {
  let left = 0;
  let right = height.length - 1;
  let leftMax = 0;
  let rightMax = 0;
  let water = 0;

  while (left <= right) {
    if (height[left] <= height[right]) {
      leftMax = Math.max(leftMax, height[left]);
      water += leftMax - height[left];
      left++;
    } else {
      rightMax = Math.max(rightMax, height[right]);
      water += rightMax - height[right];
      right--;
    }
  }

  return water;
}
Keep this sentence:Solve the side whose boundary is already known to be limiting.Time O(n) · Space O(1)
02 / Buckets

Top K Frequent Elements

Question: which values occur most often?

Input[1, 1, 1, 2, 2, 3]
→ count →
Map1:3 · 2:2 · 3:1
→ invert →
Buckets1:[3] · 2:[2] · 3:[1]
→ back →
Top 2[1, 2]

The first structure answers “how often does this value appear?” The second structure reverses that relationship and answers “which values appeared this many times?” That reversal is the useful step. Once frequency becomes an array index, the most frequent values live near the end, so retrieving the top values is a backward walk rather than a full comparison sort.

Why are there n + 1 buckets?

A value cannot appear more than the length of the input. For an array of six items, valid frequencies are 1 through 6. Index 0 stays unused, which makes the code easier to read because bucket index 3 literally means “appeared three times.” We distribute every distinct value once, then visit buckets from high frequency to low frequency until the result contains k values.

function topKFrequent(nums: number[], k: number): number[] {
  const counts = new Map<number, number>();
  for (const number of nums) {
    counts.set(number, (counts.get(number) ?? 0) + 1);
  }

  const buckets: number[][] = Array.from(
    { length: nums.length + 1 },
    () => [],
  );
  for (const [number, count] of counts) buckets[count].push(number);

  const result: number[] = [];
  for (let count = buckets.length - 1; count >= 0; count--) {
    for (const number of buckets[count]) {
      result.push(number);
      if (result.length === k) return result;
    }
  }
  return result;
}
Keep this sentence:Reorganize the information around the question you need to answer.Time O(n) · Space O(n)
03 / Selection

Selection Sort

Question: which value belongs in the next permanent position?

Start
53412
Place 1
13452
Place 2
12453
Place 3
12354
Done
12345

Selection sort draws a boundary through the array. Everything left of the boundary is finished and will never move again. Everything to the right is still searchable. On each pass, scan the unresolved region, remember the index of the smallest value, and swap that value into the first unresolved position. The sorted region grows by exactly one item.

This is not a fast general-purpose sorting algorithm. For every position, it scans most of the remaining array, producing roughly n × (n - 1) / 2 comparisons. It is still an excellent teaching algorithm because the invariant is visible: after pass p, the first p values are in their final sorted positions.

function selectionSort(values: number[]): number[] {
  for (let position = 0; position < values.length - 1; position++) {
    let minimum = position;
    for (let scan = position + 1; scan < values.length; scan++) {
      if (values[scan] < values[minimum]) minimum = scan;
    }
    [values[position], values[minimum]] =
      [values[minimum], values[position]];
  }
  return values;
}
Keep this sentence:Choose the smallest unresolved value and make one position permanent.Time O(n²) · Space O(1)
04 / Compare

Watch the work grow

Linear and quadratic algorithms feel similar on tiny inputs. Their curves separate quickly.

Operations as input grows

Linear vs quadratic

Bucket contents

For [1, 1, 1, 2, 2, 3]
Two pointersEach pointer crosses the input once.n = 80 → about 80 steps
Selection sortEach position scans the unresolved suffix.n = 80 → 3,160 comparisons

Three problems. Three reusable questions.

  1. Two pointersWhich side can I safely solve now?
  2. Frequency bucketsCan I reshape the data around the answer?
  3. SelectionWhat belongs in the next permanent position?

Define the system before choosing the stack

The useful starting point for a deterministic algorithm execution and state visualization engine is not a framework diagram. It is a precise description of the workload: instrumenting TypeScript algorithms, recording state transitions, and replaying arrays, pointers, maps, trees, and return values. That sentence identifies actors, valuable state, and the operation that must remain correct. I turn it into concrete scenarios with expected inputs, outputs, latency bounds, ownership, and recovery behavior. The scenarios include routine success, degraded dependencies, duplicate requests, stale clients, and operator intervention. Only then can a component earn its place. A database, queue, model, contract, or cache is selected because a scenario requires its guarantees, not because the technology is fashionable or familiar.

The system serves students, educators, interview candidates, engineers debugging implementations, and reviewers explaining correctness. Each group observes a different slice of the same execution. A user sees an action and an outcome. An operator sees queues, retries, policy decisions, and unresolved work. An engineer sees traces and resource limits. A reviewer needs evidence that a rule was applied. Architecture must reconcile those perspectives around one state model. If the interface says an action succeeded while an asynchronous worker can still abandon it, the product has created a false contract. I document when state becomes accepted, durable, visible, reversible, and final so every layer uses the same vocabulary.

The core invariant is simple to state and difficult to preserve: the visual timeline is a faithful deterministic representation of the exact code execution and input supplied by the user. I write invariants before APIs because endpoints can change while correctness conditions should survive. Every state transition has a precondition, an idempotency rule, an authorization decision, a durable record, and an observable result. The design review then asks whether each component preserves the invariant during concurrency and failure. This changes technical discussion from preferences about tools into claims that can be tested. It also exposes where the architecture depends on timing, undocumented provider behavior, or a single process remaining alive.

Model state as transitions, not tables

Data modelling starts with transitions. I list the states an operation can occupy and the events allowed to move it forward. Pending, accepted, running, retryable, failed, and complete are not universal names, but the distinction between them matters. Each transition records who requested it, which version of policy and input was used, when it occurred, and whether another transition may supersede it. Tables and documents follow from that state machine. This avoids the common pattern where a nullable column gradually becomes an undocumented workflow and application code invents contradictory interpretations of the same row.

At the boundary between user code, sandbox instrumentation, event serialization, trace storage, and data-structure-specific rendering, identifiers must remain stable across different consistency models. I use globally unique operation identifiers, explicit causation identifiers, and idempotency keys that reflect the caller's intent rather than a transport retry. Events carry schema versions and enough context to be interpreted after the producing code has changed. Mutable projections are rebuilt from durable facts where the cost permits it; where it does not, mutations still emit an audit record. This makes reconciliation possible because the system can explain how it reached a state instead of presenting only the latest value.

Concurrency is treated as ordinary behavior. Two workers may observe the same available work, a client may retry after a lost response, and an operator may intervene while automation is still running. Compare-and-swap updates, database constraints, leases, sequence numbers, or contract-level nonces provide serialization at the narrowest useful boundary. I avoid distributed locks unless the invariant truly requires global exclusion because locks move failure into lease management and clock assumptions. When duplicate computation is cheaper than coordination, the system allows duplicates and makes the commit path idempotent.

Design the failure path first

The representative failure set is missing mutations, reordered events, non-deterministic output, unsafe code execution, excessive trace size, and misleading visual mappings. These conditions are written as test cases before production traffic creates them. For each one, I decide whether the operation should retry, pause, compensate, escalate, or become permanently failed. Retry is never a generic error handler. It needs an error classification, an attempt budget, exponential backoff with jitter, and a deadline beyond which more work makes the outcome worse. Permanent failures retain enough evidence to diagnose and safely replay them after the cause is corrected.

Timeouts are ambiguous, not definitive. When a caller stops waiting, the remote operation may have failed, completed, or remained in progress. The system therefore queries by stable operation identifier before issuing another side effect. External providers are wrapped in adapters that normalize their states without hiding provider-specific evidence. Circuit breakers limit damage when a dependency degrades, while bounded queues prevent healthy components from accepting more work than downstream capacity can finish. Backpressure appears in product behavior so overload becomes a controlled state rather than a mysterious spinner.

Recovery is a product capability. Operators need a queue of unresolved operations, the evidence behind each state, and safe actions such as retry, cancel, reassign, or mark reconciled. Those actions use the same domain commands and authorization rules as automated paths; an admin dashboard must not become an untested second backend. Runbooks link symptoms to queries, dashboards, and recovery commands. Periodic reconciliation independently compares durable intent with external reality. That process catches the failures no synchronous path anticipated and provides a measurable upper bound on inconsistency.

Make security part of the execution model

The threat model includes infinite loops, memory exhaustion, malicious code, prototype escape, oversized inputs, and traces that expose data outside the sandbox. I map trust boundaries, data classifications, credentials, signing authority, and every component capable of producing an irreversible effect. Authorization happens at the domain operation, not only at the HTTP edge, because work may also arrive from queues, scheduled jobs, tools, or administrative interfaces. Policies are versioned and decisions are logged with the relevant subject, resource, action, and context. Short-lived credentials and narrowly scoped service identities reduce the value of a compromised worker.

Input is validated at each boundary using a strict schema and a bounded representation. File sizes, recursion, decompression ratios, numeric ranges, URL destinations, and execution time are constrained before expensive work begins. Untrusted workloads run with limited network access, isolated storage, resource quotas, and no ambient cloud credentials. Output receives the same suspicion as input: files, model responses, transaction receipts, and provider callbacks are checked against expected structure and the operation that caused them before they can change trusted state.

Sensitive data is minimized before it is encrypted. Collection requires a purpose, retention has an expiry, and logs exclude secrets and unnecessary personal fields. Encryption in transit and at rest protects storage and transport, but access patterns, identifiers, and timing can still leak information. That matters especially where fine-grained traces improve explanation while increasing runtime overhead, storage, rendering cost, and cognitive noise. I treat privacy as an architectural property with tests and observable controls. Audit trails are append-oriented, access to them is itself audited, and deletion workflows distinguish removable personal data from records that must remain for integrity or settlement.

Observe decisions, not only machines

Infrastructure metrics are necessary but insufficient. CPU usage cannot explain why an operation remained pending or why a policy denied a tool call. I instrument domain transitions using structured events that include the operation identifier, previous and next state, decision reason, duration, attempt, and responsible component. Traces cross synchronous requests, queues, workers, and provider calls using propagated context. Logs are designed for questions an operator will ask during an incident, not as a transcript of every line the code executed.

The primary measurements are trace fidelity, instrumentation overhead, replay latency, maximum safe step count, deterministic reproduction, and visualization correctness. Each metric has a definition, source of truth, expected range, and owner. Percentiles are used for latency because averages hide the users experiencing the slowest path. Queue age is separated from processing time. Success is measured at the final trustworthy outcome rather than the first accepted request. Error budgets connect reliability to release decisions: when a service consumes its budget, the team reduces change risk and invests in the failure mode instead of normalizing the alert.

Dashboards follow the lifecycle of work. They show arrival rate, accepted work, active execution, retries, terminal outcomes, and reconciliation gaps. Alerts fire on symptoms that require action, with thresholds based on user or system impact. High-cardinality identifiers remain available in traces without exploding metric cost. Synthetic operations continuously exercise critical paths, while canaries expose a new release to limited traffic. During an incident, one link should move from alert to dashboard to relevant traces and a runbook; every additional search delays recovery.

Performance is a budget with trade-offs

Performance work begins with a budget allocated across a TypeScript parser, sandbox runtime, instrumentation hooks, event schema, timeline store, and array, graph, tree, and pointer views. The end-to-end target is divided into network, queue, processing, provider, storage, and rendering or response time as appropriate. That prevents one component from celebrating a local benchmark while the complete path remains slow. Load tests reproduce realistic payload distributions, concurrency, cache state, and dependency behavior. They include sustained load to reveal leaks and saturation, not only short bursts that fit comfortably inside buffers.

I profile before optimizing. Traces identify the span consuming wall time; runtime profiles identify CPU and allocation hot paths; database plans identify scans, lock waits, and cardinality errors. Capacity models connect arrival rate, service time, and concurrency so scaling decisions can be explained. Batching improves throughput but adds waiting time. Caching reduces repeated computation but introduces invalidation and staleness. Parallelism reduces critical-path latency until coordination, provider quotas, or shared resources become the bottleneck. Every optimization names the cost it introduces.

Tail behavior determines perceived reliability. A small percentage of slow operations can occupy workers, grow queues, and trigger retries that amplify overload. Deadlines propagate through component calls so downstream work knows when its result is no longer useful. Concurrency limits isolate tenants and workload classes. Large or uncertain jobs move to asynchronous execution with honest progress states. Autoscaling uses work indicators such as queue age and estimated remaining cost, not CPU alone. The objective is predictable completion under stated constraints, not the largest benchmark number produced in an empty environment.

Ship through compatibility and evidence

Deployment design assumes versions overlap. Producers and consumers change independently, mobile clients remain old, workers finish jobs after a release, and database migrations span several deployments. Schemas evolve additively before old fields are removed. Readers tolerate both forms during transition, writers switch only after compatibility is deployed, and cleanup happens after evidence shows the old path is unused. Feature flags separate deployment from release but require ownership and expiry; otherwise they become permanent branches through every important path.

Risky changes use shadow execution, sampled comparison, or a limited canary. A new planner can evaluate real workloads without assigning resources. A new scoring rule can produce results beside the current rule. A migration can compare counts and checksums before traffic switches. Rollback is defined for code, configuration, schema, and external side effects; "redeploy the previous image" is not enough when the new version has already emitted events or changed durable state. Each release records the artifact, configuration, migration, and feature state needed to reconstruct behavior.

Tests concentrate on contracts and failure. Unit tests cover state transitions and policy decisions. Property tests explore invariants across generated sequences. Integration tests use real databases, queues, and representative provider adapters. Deterministic fault injection drops acknowledgements, expires leases, delays dependencies, duplicates messages, and interrupts workers. End-to-end tests confirm that the visual timeline is a faithful deterministic representation of the exact code execution and input supplied by the user. Production verification then checks metrics and traces for the expected behavior. Passing before deployment is evidence; stable behavior after deployment is the actual result.

Keep the architecture able to change

The most durable architecture is not the one with the most abstraction. It is the one whose important boundaries match the problem. I keep domain state and policy independent from transport, providers, and framework conventions. Adapters isolate external APIs where their semantics differ. Internal modules remain together until independent scaling, ownership, security, or release cadence justifies a service boundary. A modular deployment can preserve clearer transactions and simpler operations while still leaving room to extract a component when evidence demands it.

Technical debt is recorded as a violated expectation with impact, not as general discomfort. "This queue has no dead-letter replay" or "this score cannot be recomputed from versioned inputs" creates a testable improvement. Architecture decisions capture context, alternatives, consequences, and a review trigger. Capacity assumptions and provider limits live beside the decision. This matters because the system will outlive the memory of the meeting where a compromise was made, and future engineers need to know whether a constraint remains real.

For this deterministic algorithm execution and state visualization engine, the central trade-off remains: fine-grained traces improve explanation while increasing runtime overhead, storage, rendering cost, and cognitive noise. There is no permanent resolution, only a decision appropriate to current users, scale, risk, and operational capacity. I revisit that decision when metrics, incidents, provider behavior, or product requirements change. The final engineering skill is not predicting every future condition. It is building enough explicit state, evidence, isolation, and recovery that the team can learn what is true and change direction without gambling the integrity of the system.

References and inspiration

Learn Trapping Rain Water, Top K Frequent and Selection Sort with Step-by-Step Visualization ↗