Functional Calm Architecture

Calm architecture is often described through folders, packages, services, APIs, deployment boundaries, and naming conventions.

That is all important.

But calm architecture is also about how execution flows through a system.

A project can have clean folders and still behave chaotically if its scripts start too many things at once, retry in random places, poll aggressively, hide important behavior behind magic helpers, or fail the whole process because one remote system was not ready for three seconds.

This is especially visible in automation code:

  • smoke-tests
  • deploy checks
  • infrastructure scripts
  • content generation scripts
  • CDN validation
  • API verification
  • post-build workflows

Those scripts usually talk to systems outside our process:

  • CDNs
  • APIs
  • object storage
  • preview environments
  • serverless functions
  • package registries
  • internal services

Remote systems are not always immediately consistent. They can be slow, temporarily unavailable, cold, throttled, or still propagating a change.

A calm script does not pretend that the world is perfectly synchronous.

It makes the flow explicit.

It controls order. It retries temporary failures. It adds small pauses when needed. It uses local concurrency only where it is safe. It hides boring HTTP details behind one clear contract.

That is where small functional primitives become very practical.

Not as a religion. Not as "everything must be point-free". Not as a reason to avoid readable code.

Functional ideas are useful here because they help us build small, explicit, composable units of behavior.

Functional primitives instead of a big framework

For many automation flows, a big framework is too much.

What we usually need is not a new platform. We need a few small primitives that make the flow easy to read:

import { pause } from '@vyriy/pause';
import { recursive } from '@vyriy/recursive';
import { request } from '@vyriy/request';
import { retry } from '@vyriy/retry';

await recursive(async (item) => {
  await retry(async () => {
    await Promise.all([request(item.htmlUrl), request(item.assetUrl)]);
  }, retryOptions);

  await pause(500);
}, items);

This example looks small, but it describes an important architecture pattern.

Each primitive has one responsibility:

  • recursive controls the order.
  • retry handles temporary instability.
  • Promise.all allows local parallelism for independent work.
  • pause reduces pressure between steps.
  • request gives HTTP calls one shared contract.

Together, they create a calm flow.

The code does not say: "Run everything as fast as possible and hope remote systems survive."

It says: "Process one item at a time. For each item, check independent resources in parallel. Retry temporary problems. Then wait a little before touching the next item."

That is a very different operational attitude.

Flow control is architecture

When people talk about architecture, they often start with static structure:

  • packages
  • modules
  • folders
  • services
  • boundaries
  • dependencies

That is useful, but incomplete.

Runtime flow is architecture too.

A script that validates assets, checks APIs, warms functions, publishes data, waits for propagation, or verifies an environment is part of the architecture. It defines how the system behaves under change.

Calm architecture asks practical questions:

  • Is the order of execution obvious?
  • Is concurrency intentional?
  • Are remote systems protected from unnecessary pressure?
  • Are temporary failures handled explicitly?
  • Are HTTP rules consistent?
  • Can a developer understand the flow without opening ten files?
  • When it fails, does it fail with useful context?

Small functional primitives help answer "yes" to those questions.

They make the execution model visible.

recursive: predictable sequential flow

The first important choice is order.

Many scripts start with a simple question:

Should we process all items at once, or one item at a time?

For a smoke-test after deployment, "all at once" can be dangerous.

If there are 50 pages and each page checks an HTML URL plus several assets, a naive implementation can suddenly create hundreds of requests against a CDN, a serverless endpoint, or a freshly deployed service.

That may be fine in a local unit test. It may be noisy in production-like infrastructure.

A sequential primitive such as recursive makes the decision visible.

await recursive(async (item) => {
  await checkItem(item);
}, items);

This is not about recursion as a clever trick.

It is about expressing a controlled flow:

  1. Take the current item.
  2. Process it.
  3. Move to the next item only after the current item is done.

An imperative loop can do the same thing, and that is okay:

for (const item of items) {
  await checkItem(item);
}

The value of a small recursive utility is not that it is magically better than for.

The value is that it gives a name to a reusable behavior: sequential processing.

The calm architecture idea is simple:

Sequential work should look sequential.

There should be no hidden concurrency. No accidental map(async ...) without awaiting correctly. No surprise request storm.

Just one item after another.

retry: respect temporary instability

Distributed systems are rarely stable in the first second after a change.

A deployment can finish before every edge location is updated. A serverless function can be cold. A DNS or CDN change can need time to propagate. A newly created resource can exist in one API response but not yet be ready in another.

This is not always a real failure.

Sometimes it is just time.

That is why retry is a core calm primitive.

await retry(async () => {
  await request(item.htmlUrl);
}, retryOptions);

Without retry, the script is strict in the wrong way.

One temporary 503, one timeout, or one not-yet-ready response can fail the whole flow even though the system would be healthy a few seconds later.

With retry, the script becomes more realistic:

This operation must succeed, but it is allowed to become true shortly after the system changes.

That is especially useful for:

  • smoke-tests
  • CDN validation
  • serverless warm-up checks
  • package availability checks
  • object storage reads after upload
  • service-to-service readiness checks
  • eventual consistency after infrastructure changes

The important part is that retry should be explicit.

A calm retry has visible options:

const retryOptions = {
  attempts: 5,
  delay: 1000,
  timeout: 5000,
};

The exact shape depends on the package, but the intent should be obvious:

  • how many attempts are allowed
  • how long to wait between attempts
  • which errors are considered temporary
  • when to stop

Retry should not hide broken systems forever.

It should absorb temporary instability, not silence real failures.

pause: small backpressure between steps

Retry helps when one operation is temporarily unstable.

pause helps with the rhythm of the whole flow.

await pause(500);

A pause between items is a tiny form of backpressure.

It says:

We do not need to hit the remote system as aggressively as possible.

That is a very calm architecture decision.

In automation scripts, speed is not the only value. Predictability matters. Being kind to remote systems matters. Avoiding rate limits matters. Avoiding false negatives matters.

A small pause can reduce pressure on:

  • APIs with rate limits
  • CDNs still propagating files
  • serverless functions after deployment
  • preview environments with limited resources
  • internal tools that were not designed for heavy parallel polling

The pause does not need to be dramatic. Sometimes 300–500 milliseconds is enough to make the flow less noisy.

This is also a cultural signal in code.

A script with explicit pause is easier to reason about than a script with accidental pressure hidden inside nested async calls.

Calm architecture is not passive. It is controlled.

Promise.all: local parallelism, not chaotic concurrency

Sequential processing does not mean everything must be slow.

Inside one item, some operations are independent.

For example, checking the HTML page and checking a static asset can happen at the same time:

await Promise.all([
  request(item.htmlUrl),
  request(item.assetUrl),
]);

This is a good use of Promise.all.

The parallelism is local and bounded.

We are not processing every item in parallel. We are only running independent checks inside the current item.

That distinction matters.

Chaotic concurrency looks like this:

await Promise.all(
  items.map(async (item) => {
    await request(item.htmlUrl);
    await request(item.assetUrl);
  }),
);

This may be fine for a small list. But as the list grows, it can quickly become noisy. The number of concurrent requests becomes a side effect of the input size.

A calmer flow separates the two decisions:

  1. Across items: sequential.
  2. Inside one item: parallel where safe.
await recursive(async (item) => {
  await Promise.all([
    request(item.htmlUrl),
    request(item.assetUrl),
  ]);
}, items);

This is one of the most useful patterns in infrastructure and verification scripts:

Use concurrency where it is local, obvious, and bounded.

Do not let array size accidentally become your concurrency policy.

request: one HTTP contract

HTTP calls are repetitive.

Every script eventually needs the same decisions:

  • What is a successful response?
  • Should non-2xx responses throw?
  • Should the body be parsed as JSON or text?
  • What timeout should be used?
  • How are errors shaped?
  • Are retries handled here or outside?
  • How much context should be included in an error message?

If every script writes its own fetch logic, the project slowly collects many slightly different HTTP behaviors.

That is not calm.

A small request wrapper gives the project one shared HTTP contract:

await request(item.htmlUrl);

The caller does not need to repeat the same ceremony every time.

The wrapper can own the boring details:

export async function request(url: string, options?: RequestOptions) {
  const response = await fetch(url, {
    signal: options?.signal,
    headers: options?.headers,
  });

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status} ${url}`);
  }

  return response;
}

In a real package, the wrapper can support more:

  • timeout
  • JSON parsing
  • text parsing
  • typed response helpers
  • custom headers
  • retry integration
  • better error messages
  • response validation

The important point is architectural:

HTTP behavior should be consistent across scripts.

When smoke-tests, deploy checks, and infrastructure helpers use the same request primitive, the system becomes easier to debug.

There are fewer hidden differences. Fewer one-off fetch wrappers. Fewer surprising error shapes.

A small smoke-test flow

Imagine a static or SSR deployment.

After deployment, we want to validate that important pages and assets are available:

type SmokeItem = {
  name: string;
  htmlUrl: string;
  assetUrl: string;
};

const items: SmokeItem[] = [
  {
    name: 'Home',
    htmlUrl: 'https://vyriy.dev/',
    assetUrl: 'https://vyriy.dev/assets/app.css',
  },
  {
    name: 'Blog',
    htmlUrl: 'https://vyriy.dev/blog/',
    assetUrl: 'https://vyriy.dev/assets/blog.css',
  },
];

A calm smoke-test can look like this:

import { pause } from '@vyriy/pause';
import { recursive } from '@vyriy/recursive';
import { request } from '@vyriy/request';
import { retry } from '@vyriy/retry';

const retryOptions = {
  attempts: 5,
  delay: 1000,
};

await recursive(async (item) => {
  console.log(`Checking ${item.name}`);

  await retry(async () => {
    await Promise.all([
      request(item.htmlUrl),
      request(item.assetUrl),
    ]);
  }, retryOptions);

  await pause(500);
}, items);

The behavior is easy to explain:

  1. Pick one smoke-test item.
  2. Check its page and asset in parallel.
  3. Retry if the environment is not ready yet.
  4. Pause before moving to the next item.
  5. Fail clearly if the item still does not become healthy.

This is not a large framework. It is not a complex pipeline engine. It is just a few small primitives composed together.

But the result is a script with a clear operational personality.

It is careful. It is predictable. It is readable. It is friendly to remote systems.

That is calm architecture.

Functional does not mean abstract

Functional programming can sometimes feel abstract because it is often introduced through academic examples or terminology-heavy explanations.

But the practical side is much simpler.

In this context, functional style means:

  • behavior is passed as a function
  • small units do one thing
  • composition is preferred over hidden global state
  • flow is visible from the call site
  • utilities are reusable without becoming a framework

The example is functional because recursive and retry receive behavior:

await recursive(async (item) => {
  await retry(async () => {
    await checkItem(item);
  }, retryOptions);
}, items);

But it is still ordinary TypeScript.

There is no need to turn the codebase into a functional programming exercise.

A calm codebase can use functional ideas where they make the system clearer, and use simple imperative code where it is more readable.

The goal is not purity.

The goal is low cognitive load.

From primitives to systems

Small functional primitives are useful on their own, but they become even more powerful when they shape larger automation layers.

The same ideas can guide deployment and smoke-test scripts:

  • narrow responsibilities
  • explicit order
  • bounded concurrency
  • retries around unstable systems
  • pauses between remote operations
  • one shared contract for repeated behavior

That is how functional calm architecture scales.

Not by adding abstraction for its own sake. Not by hiding the flow. Not by turning scripts into a framework.

But by making each part small enough to understand and explicit enough to trust.

Small decisions create calm systems

Calm architecture is not created by one big decision.

It is created by many small decisions that make the system easier to understand and operate.

A small request wrapper removes HTTP duplication. A small retry helper makes temporary instability explicit. A small pause helper adds backpressure. A small recursive helper gives a name to sequential flow. A native Promise.all gives local parallelism where it is safe.

None of these primitives is impressive alone.

Together, they shape the behavior of automation flows.

They make the system less surprising.

And that is the point.

Calm architecture is not only about where files live. It is also about how work moves.

One item at a time. Parallel only where it is safe. Retry when the world is temporarily unstable. Pause when remote systems need breathing room. Use one clear contract for repeated operations.

Small primitives. Explicit flow. Predictable behavior.

That is functional calm architecture.