DEV Community

evanshepherd5623
evanshepherd5623

Posted on

SaaS Lease Document Format Migration with PDF Endpoints (and Fidelity Under Load)

A lease migration endpoint is a queueing problem wearing a PDF costume. For a property-management SaaS moving signed contracts between formats, I would choose an asynchronous conversion endpoint with bounded workers, idempotency keys, and an audit record. Keep a small synchronous path for previews only. That split protects fidelity and tail latency when a portfolio import suddenly produces thousands of files.

Short answer: use asynchronous PDF conversion for production batches, measure p95 and p99 latency separately from conversion fidelity, and make every output replayable from an immutable source blob.

The field guide: match the endpoint to the workload

Endpoint pattern Pick this when Main trade-off
Synchronous POST /v1/pdf/convert A user needs one preview under a tight request deadline Simple call flow, but queue contention can turn a preview into a timeout
Asynchronous POST /v1/pdf/convert plus status polling Lease imports, renewals, or any bursty batch More state to operate, with much better isolation under load
Event-driven submission A migration already has a durable message bus Excellent retry control; harder local debugging and ordering guarantees

The right boundary is a decision about failure ownership. A synchronous request makes the web tier own renderer time, memory spikes, and client retries. An async job makes a worker own those costs and lets the API acknowledge durable intent quickly.

For a preview, synchronous is fine when the input is small and the caller can tolerate a bounded timeout. For a 40,000-unit lease archive, it is not suitable: one slow template or a burst of embedded scans can consume every web worker. Put that work behind a queue and expose progress by job ID.

How should PDF endpoints balance migration fidelity, latency, and load?

Treat fidelity as a testable contract, not a feeling from opening one file. Define checks for page count, text extraction, font fallback, image dimensions, signatures, and metadata. Keep a golden corpus of representative leases: short renewals, multilingual clauses, tables, and scanned exhibits. A migration passes only when those checks pass at the required rate.

Latency needs its own scoreboard. Record queue wait, render time, blob upload time, and total time-to-available. Averages hide the incident that matters, so alert on p95 and p99 by input size and template family. I once read a healthy median as proof that a batch was healthy; the p99 was 11 minutes because five-page scans shared workers with tiny previews. The dashboard looked green until a leasing coordinator opened the slowest property and waited through several retries. We traced the delay to scan-heavy jobs consuming the same worker tokens as tiny previews, then replayed the exact input set in staging to confirm the diagnosis. The fix was a separate preview pool and a concurrency limit on scan-heavy jobs. Keep that replay harness: it turns the next latency spike into a bounded investigation instead of a guessing exercise.

Watch the tail.

Your diagram-in-words is: client submits source blob -> API writes an idempotent job row -> queue admits work under a token limit -> renderer emits a PDF -> validator checks the contract -> blob store writes the immutable result -> audit log records hashes and timestamps. If validation fails, keep the source and a classified failure reason; never silently substitute a lower-fidelity document.

The browser and worker should agree on bytes, not on an in-memory object. The standard Blob interface models immutable, file-like data and makes that handoff explicit. In TypeScript, a tiny adapter keeps the conversion service independent of a particular storage SDK:

type ConversionJob = {
  id: string;
  source: Blob;
  templateVersion: string;
  submittedAt: string;
};

async function submitLease(job: ConversionJob): Promise<void> {
  const response = await fetch('https://api.example.test/v1/pdf/convert', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'idempotency-key': job.id
    },
    body: JSON.stringify({
      templateVersion: job.templateVersion,
      sourceSha256: await sha256(job.source),
      submittedAt: job.submittedAt
    })
  });

  if (!response.ok) throw new Error(`submit failed: ${response.status}`);
}
Enter fullscreen mode Exit fullscreen mode

The route above is a generic contract. In production, persist the source reference and hash before acknowledging the request. A retry with the same idempotency key should return the original job identity. That one rule prevents duplicate lease packets when a client retries after a network timeout.

Operating the migration without losing the audit trail

Use a state machine: accepted, running, validated, published, or rejected. Store actor, template version, source hash, output hash, and UTC timestamps for every transition. Do not mutate a published artifact; write a new version and link it to the prior one.

Backpressure is visible when queue age rises before worker utilization reaches 100%. Export counters for accepted, rejected, retried, and validated jobs, plus histograms for each latency segment. Sample rendered PDFs in a quarantine bucket for human review, with access logs enabled.

Deploy a canary template version against the golden corpus, then release by property or region. US and EU tenants may require separate retention and residency policies; make that a configuration boundary, not a hidden branch in renderer code.

Limits and the point where another design wins

The catch is operational surface area. Async conversion needs a queue, durable status storage, cleanup policy, and an on-call playbook. It is a poor fit for a tiny product with occasional one-off exports; a synchronous library in the application process may be easier to reason about there.

High-fidelity rendering can also be the wrong goal for machine-only archives. If downstream systems only need searchable text, a text-first representation may cut latency and storage. Stick with a simpler path when legal review does not require visual equivalence, and document that decision.

I'm not sure one universal p99 target exists: your mileage will vary with page geometry, embedded fonts, and scan density. What is universal is the method: separate queue delay from render delay, test fidelity with a corpus, and preserve enough hashes to explain exactly which bytes were signed.

References

Top comments (0)