Short answer: use a document-render endpoint for owned invoice pages, a merge endpoint for finished source PDFs, and an asynchronous composition endpoint for the complete multi-source board book. Give every source a fidelity rule before traffic arrives; under load, preserve required financial pages and shed optional rendering work rather than quietly changing the document.
The least complex path is three stages. Order data becomes a validated invoice view model, that model becomes a PDF, and the finished invoice joins already-rendered finance and operations sections in a board pack. This division matters because HTML, structured order data, and an existing PDF are different inputs. Sending all three through one universal renderer buys a tidy-looking integration while hiding which step consumed the latency and which step changed the pixels.
The concrete decision is about ownership. If the SaaS owns the template, it can render from structured data. If another system owns a page, preserve its PDF bytes unless there is a documented reason to reconstruct them. Simple first.
Put page ownership into the executable contract
Model the document before choosing a remote endpoint. The model below makes required pages, source ownership, and fidelity visible. It doesn't contain a vendor route, because those fields should survive a renderer replacement. The renderOwned and mergePdf functions are adapters: one accepts a controlled view model, while the other combines finished bytes.
type Order = {
id: string;
customerName: string;
currency: "USD" | "EUR";
lineItems: Array<{ description: string; quantity: number; unitPrice: number }>;
};
type BoardSection =
| { kind: "owned-invoice"; order: Order; required: true }
| { kind: "source-pdf"; name: string; bytes: Uint8Array; required: boolean };
type PdfAdapters = {
renderOwned: (order: Order) => Promise<Uint8Array>;
mergePdf: (parts: Uint8Array[]) => Promise<Uint8Array>;
};
function assertOrder(order: Order): void {
if (!order.id || !order.customerName || order.lineItems.length === 0) {
throw new Error("The invoice source is incomplete");
}
for (const item of order.lineItems) {
if (item.quantity <= 0 || item.unitPrice < 0) {
throw new Error("The invoice contains an invalid line item");
}
}
}
async function buildBoardBook(
sections: BoardSection[],
adapters: PdfAdapters,
): Promise<Blob> {
const rendered: Uint8Array[] = [];
for (const section of sections) {
if (section.kind === "owned-invoice") {
assertOrder(section.order);
rendered.push(await adapters.renderOwned(section.order));
continue;
}
rendered.push(section.bytes);
}
const bytes = await adapters.mergePdf(rendered);
return new Blob([bytes], { type: "application/pdf" });
}
The browser Blob API represents immutable raw data and accepts array-buffer-backed input, so it is a clean return type at the download boundary. It shouldn't be the internal business model. Keeping Order, BoardSection, and the PDF bytes separate prevents display concerns from leaking into validation.
There is an intentional omission in this example: it doesn't decide what to do with an unavailable optional section. That is a product policy, not a catch block. A board book prepared for directors may require every approved section; an internal preview may permit a clearly marked omission. Encode that choice in the job request and audit record rather than letting an adapter improvise.
How should SaaS use PDF endpoints for multi-source board books under load?
Require input semantics that match the material, plus a completion model that matches its cost. An owned invoice template needs a render operation. A signed or externally generated source PDF needs byte-preserving composition. A multi-source board book needs a job boundary because its work grows with the number and type of sections, even if individual invoice renders are quick.
| Endpoint capability | Best input | Fidelity risk | Load behavior | Operational cost |
|---|---|---|---|---|
| Structured-data render | Validated order model and controlled template | Template changes can move pagination | Predictable work is easier to classify | Template and font lifecycle stay with the SaaS |
| HTML-to-PDF render | HTML and assets owned by the caller | Browser, fonts, and print CSS affect output | Asset retrieval adds variable work | Browser isolation and asset policy need attention |
| PDF composition | Finished PDF byte streams | Re-rendering should be avoided | Work follows source size and page count | Ordering, validation, and provenance become central |
| Asynchronous document job | Mixed sections with explicit policies | Fidelity depends on each stage | Admission can be separated from completion | Status, retention, cancellation, and cleanup need contracts |
This isn't a ranking. One board book can legitimately use all four capabilities. The useful selection test is whether an endpoint transforms content or only composes it. A transformation deserves visual regression tests; composition deserves checks for page order, count, media boxes, and retained source identity.
For US/EU operation, keep region selection and data retention explicit in the adapter contract and deployment configuration. Don't infer either property from a hostname or a sales label. The evidence needed is the service contract and the actual deployment configuration; without those, I'm not sure a team can make a defensible residency claim. This is also where a self-managed renderer can be the right answer: it offers placement control, but the team then owns patching, fonts, browser processes, capacity, and cleanup.
Spend the latency budget on transformations, not handoffs
Tail latency under load gets easier to reason about when every stage has a bounded purpose. Validate order fields once. Render owned invoice pages once. Preserve completed source PDFs. Compose at the end. That shape removes accidental transformations, such as rasterizing an entire finance section merely to add it to a pack, and it creates a cache boundary around artifacts whose inputs and template version have not changed.
Use a per-document render budget rather than a single timeout copied across every call. The budget is a policy chosen from the product's service objective and measured workload, not a universal number. Divide it among source acquisition, owned-page rendering, and final composition, then record time spent in each stage. Queue wait belongs in its own field. Otherwise a capacity problem looks like a slow template, and an engineer will optimize the wrong thing.
Under load, classify work before it reaches a renderer. A one-page invoice, a board pack made entirely from finished PDFs, and a pack containing several browser-rendered sections shouldn't compete as if they consume equal resources. Separate concurrency controls can protect small interactive exports from large scheduled packs. Backpressure must happen at admission: accept work only when the system can honor its completion policy, or return a retryable response defined by the application contract. Don't spawn unbounded browser processes.
Cost follows the same boundaries. Browser rendering usually carries more moving parts than byte composition, while reusing an unchanged artifact avoids both render work and visual drift. Cache keys therefore need the normalized input, template version, locale, font set, and rendering policy. A hash of raw request JSON is too fragile because harmless key ordering can change it, while an incomplete key can serve yesterday's template as today's invoice.
Fast is conditional.
The catch is that tiered work classes add scheduling and observability overhead. They are not suitable for a low-volume SaaS whose documents are short, controlled, and comfortably handled by one process. Stick with a synchronous owned-template renderer in that case, provided the caller can tolerate its measured worst-case render time. Move to asynchronous composition when document size, mixed ownership, or burst traffic makes request duration unpredictable. The decision should follow load tests using the actual document mix, not a vendor's median demo.
Make fidelity failures impossible to wave away
PDF generation can succeed at the transport layer and still produce the wrong invoice. Test semantic output and visual output separately. Semantic checks should extract and compare the order identifier, currency, line-item totals, tax fields, and page sequence. Visual checks should use approved fixtures to catch clipped rows, substituted fonts, missing logos, and moved page breaks. Neither test replaces a final review of legally significant templates.
Keep fixture variation purposeful: USD and EUR orders, long customer names, a line item that wraps, a multi-page invoice, a source PDF with a different page size, and a board pack containing optional and required sections. Those are test categories, not claims about a renderer. Add the cases that production inputs reveal, but don't turn a transient example into an undocumented compatibility promise. Then connect every produced artifact to its normalized input, template version, source revisions, render policy, region, and checksum. Log identifiers and stage timings while leaving invoice content out of logs. A retry may reuse a successful immutable stage only when its full cache key still matches; otherwise, rerender it. Cancellation needs an equally explicit rule for whether partial artifacts disappear immediately or through the normal retention process. This fixture-to-record chain is worth tracing in a preproduction review because it catches a subtle class of mistakes: a PDF can look correct, contain the expected text, and still lack the provenance needed to explain which source revision or template produced it.
Deployment is where the endpoint abstraction earns its keep. Run the same fixture suite against a candidate adapter, compare semantic results, inspect visual differences, and then exercise the expected mixture under constrained concurrency. A provider swap is safe only when output and operational behavior meet the same contract.
Signatures can lie.
Before release, read the workflow as one continuous transaction: input validation rejects incomplete financial data; source ownership selects render versus composition; required-section policy controls completion; stage budgets expose saturation; fixture tests guard meaning and appearance; retention removes artifacts on schedule. If any one of those decisions exists only inside a worker implementation, pull it into configuration or the job schema. That's the boring work that keeps a PDF endpoint from becoming an opaque production dependency.
Further reading
- MDN Web Docs, "Blob": https://developer.mozilla.org/en-US/docs/Web/API/Blob
Top comments (0)