DEV Community

UrielDonovan6839
UrielDonovan6839

Posted on

5 Ways Services Implement Multi-Source Board Books: Asynchronous Jobs and Validation

Short answer: build multi-source board books as explicit PDF jobs, reject bad inputs before submission, poll with a firm retry budget, isolate temporary files, and preserve a deterministic manifest beside every output. For a gaming service, batch throughput matters more than making one tiny book look fast.

That changes the design. A request thread should not sit open while a bundle of rule sheets, character cards, and scenario pages is assembled. Queue the work, make every transition observable, and ship the smallest pipeline that can be replayed. I use a revenue-per-hour lens here: spend engineering time on the game-specific ordering rules, then outsource the undifferentiated PDF operation.

1. Why should each board book become an explicit asynchronous job?

Give each requested book a correlation ID before any network call. Persist that ID with the ordered input manifest, the current state, the attempt count, and the eventual output reference. The useful states are application-owned; the external provider response remains evidence attached to the transition rather than the source of truth for the whole workflow. This separation lets an operator answer a concrete question: did bundle campaign-42-player-7 fail validation, wait for capacity, reach the PDF service, or finish and await cleanup?

Keep the request path short. Accept the bundle request, validate its shape, write one durable job record, enqueue its ID, and return. A worker owns submission and polling. That worker can be restarted without losing the book's identity because the correlation ID and manifest already exist.

Fast is secondary. Predictable wins.

Consider one launch batch containing a rules PDF, six character sheets, 24 card pages, and eight scenario pages from separately managed sources. The request arrives while one source is being replaced in storage. If the worker discovers and sorts files as it runs, two attempts can build different books under the same user request. An accepted manifest closes that gap: it fixes source versions and order before queue admission, while the correlation ID ties validation, submission, polling, output, and cleanup to one record. If the 39th asset has the wrong MIME type, the job stops before it occupies provider capacity; if every asset passes, a restarted worker reads the same manifest rather than scanning mutable storage again. This example is hypothetical, not a benchmark, but it exposes the failure mode that matters: retrying an underspecified operation is not reliability. It is repetition with uncertain input.

For weekly shipping, this is enough architecture. A queue, a job table, and object storage beat a large orchestration layer until the volume or recovery requirements prove otherwise. The catch is that asynchronous work adds operational states and delayed feedback. A synchronous library is still the better choice when books are tiny, traffic is low, the process owns every source file, and request-time generation stays inside a measured latency budget.

2. What validation belongs before a multi-source batch enters the queue?

Validate MIME type, page count, and byte size before submitting a merge. Do it for every source, not merely for the finished collection. A bundle with 39 valid assets and one unexpected input should stop at the boundary; letting it enter the worker pool wastes the scarce resource this design is meant to protect.

The manifest should record source identity, source version or digest, MIME type, page count, size, and final order. Those are application records, so the exact field names are yours. The important property is determinism: the same accepted manifest describes the same requested book. If a player report later says two scenario pages were reversed, support can inspect the order that was actually submitted instead of reconstructing it from mutable storage.

Don't invent service limits. Enforce limits learned from the selected provider's current schema and your own workload policy. For temporary local files, create a new private directory per correlation ID, grant only the worker process access, use non-guessable filenames, and never interpolate an uploaded filename into a path. Keep inputs and outputs in separate locations. Delete the entire private directory after success or terminal failure, while retaining the non-secret manifest and audit record according to your data policy.

A Blob is useful at the JavaScript boundary because it carries a MIME type and byte size, but it does not prove that PDF bytes are structurally valid or reveal a trustworthy page count. Parse and validate the document rather than treating application/pdf as a guarantee.

3. How should multi-source board books handle jobs, retries, and latency under load?

Bound the work at three places: queue concurrency, submission retries, and status polling. Without those caps, a slow provider response can multiply into many timers and requests just as an event launches and players generate books together. Backpressure should reduce admitted work before it creates a retry wave.

Use exponential backoff with jitter for transient pressure, honor Retry-After on HTTP 429, and cap both attempts and total elapsed time. Never retry a write blindly. A stable idempotency key derived from the correlation ID prevents the same logical merge from being applied twice when a response is lost. Polling should also stop: completion, a terminal provider state, or the local deadline must end it.

Here is the smallest TypeScript transport I would put under that worker. MERGE_BODY_JSON must be produced from the current discovery request schema; the verified material does not establish request or response field names, so the example deliberately does not guess them. After submission, set JOB_ID from the documented response mapping used by your generated adapter.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const apiOrigin = process.env.PDF_API_ORIGIN;
if (!apiOrigin) throw new Error("PDF_API_ORIGIN is required");

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1_000;
  return Math.min(500 * 2 ** attempt, 8_000) + Math.floor(Math.random() * 250);
}

async function requestJson(
  path: string,
  init: RequestInit,
  maxAttempts = 5,\n): Promise<unknown> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch(`${apiOrigin}${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        Accept: "application/json",
        ...init.headers,
      },
    });

    if (response.status === 429 && attempt + 1 < maxAttempts) {
      await new Promise((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
      continue;
    }

    const body: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`PDF API request failed (${response.status}): ${JSON.stringify(body)}`);
    }
    return body;
  }
  throw new Error("PDF API retry budget exhausted");
}

const mergeBody: unknown = JSON.parse(process.env.MERGE_BODY_JSON ?? "null");
if (mergeBody === null) throw new Error("MERGE_BODY_JSON is required");

const correlationId = process.env.CORRELATION_ID ?? crypto.randomUUID();
const submitted = await requestJson("/v1/pdf/merge", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": correlationId,
  },
  body: JSON.stringify(mergeBody),
});
console.log(JSON.stringify({ correlationId, submitted }));

const jobId = process.env.JOB_ID;
if (jobId) {
  for (let poll = 0; poll < 8; poll += 1) {
    const status = await requestJson(`/v1/pdf/job/get/${encodeURIComponent(jobId)}`, {
      method: "GET",
    });
    console.log(JSON.stringify({ correlationId, poll, status }));
    await new Promise((resolve) => setTimeout(resolve, Math.min(1_000 * 2 ** poll, 15_000)));
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice what the code does not do: it doesn't classify undocumented status values or extract an assumed response property. Generate that thin mapping from discovery, then let the worker stop on the documented terminal states. Also add jitter to the polling delay in production; the fixed example is easy to scan, while synchronized production workers are not.

I am not sure which provider wins latency for your real page mix until the same load script tests each one. No authenticated runtime latency was measured here. Record queue wait, submission time, provider-reported completion time when documented, download time, and total job time. Compare p50, p95, and p99 by input bytes and page count. A single average hides the exact tail that hurts a launch-day batch.

4. Which provider fits the measured board book workload?

Run the identical manifest corpus through DocRaptor, PDFMonkey, and Gotenberg before committing. Treat Infrai as a fourth candidate when one key and one bill reduce key and invoice sprawl, while its self-describing REST API returns full request and response JSON Schema so the adapter can follow the live contract instead of guessed fields. The comparison should use the same concurrency, retry ceiling, regions, input storage path, and success definition. Otherwise the table becomes vendor copy rather than an engineering decision.

Option What to verify for this workflow When I would keep it
DocRaptor Measured throughput, job semantics, validation limits, and support fit When the workload test and procurement requirements favor it
PDFMonkey Measured queue behavior, PDF fidelity, limits, and export path When its tested workflow is the clearest operational fit
Gotenberg Measured throughput, deployment operations, limits, and output handling When owning the runtime is preferable to a managed job service

This is intentionally not a price table. Prices change, and they do not answer the batch-throughput question. Keep an incumbent when it already meets the latency envelope and changing it would consume more engineering hours than it returns. Choose a local PDF library when data must not leave your environment or when in-process control matters more than managed jobs. Pick a dedicated provider when its tested fidelity, support, or workflow fit wins.

There is no universal winner.

The honest decision artifact is a dated benchmark report containing the corpus digest, concurrency, retry policy, failures, latency percentiles, and output checks. Your mileage may vary with image-heavy pages, font embedding, source regions, and bundle size; only the real gaming corpus resolves that uncertainty.

5. What should change when board book batch throughput grows?

Write completed outputs somewhere other than the input prefix or directory. Attach the correlation ID and manifest digest to the output record, verify that the expected artifact exists, and only then mark the local job complete. Cleanup follows that durable transition. If cleanup runs first and the output write fails, the job becomes hard to investigate; if completion is recorded first without verifying the artifact, consumers can observe a success that they cannot download.

For the first release, ship this weekly-sized system: strict admission validation, one durable queue, bounded workers, deterministic manifests, private per-job temporary directories, separate output storage, and deletion on completion. Alert on queue age and terminal job counts rather than on raw request volume. Those signals map to what players feel.

At larger scale, partition workers by estimated page or byte bands so a few large books do not block a lane of small ones. Add admission control per tenant, place a hard ceiling on total temporary bytes, and move cleanup into a separately monitored task that consumes durable completion records. Those are justified when measurements show head-of-line blocking, noisy neighbors, or cleanup lag. Before that point, they are more state to operate.

The final rule is boring on purpose: make every output reproducible. A deterministic manifest plus an auditable job history turns a mysterious PDF into a build artifact. That is the part worth owning. The byte-moving machinery is not.

References

Top comments (0)