Leaders Logo

QuestPDF and .NET: Architectural Patterns for Document Generation in Distributed Systems

Foundation: QuestPDF and Deterministic Layout Generation

QuestPDF is a .NET library that allows generating PDFs through composition-based layout libraries (QUESTPDF, 2026). Instead of converting HTML/CSS to PDF, QuestPDF explicitly describes the hierarchy of containers and components, reducing rendering ambiguity and providing fine control over typography, spacing, grids, and tables. In practice, this shifts the "source of truth" of the layout to the code, which facilitates versioning, review, and testing in CI/CD pipelines.

From an operational standpoint, a PDF generator is sensitive to variables such as available fonts, images, and memory usage. The deterministic composition model reduces variability across environments but does not eliminate the need for asset standardization (e.g., embedding fonts in the container or distributing them as service resources). Thus, the library is part of the solution; the rest depends on the architectural design of how data is collected, how the document is generated (synchronous vs. asynchronous), and how the artifact is delivered and audited.

Materials and Methods

The adopted method is descriptive and applied, oriented towards software architecture patterns and engineering practices. It starts from the problem of document generation in distributed systems and models a reference flow: (i) data collection by domain services, (ii) composition of the generation payload, (iii) PDF generation via a dedicated service, and (iv) delivery/storage of the artifact. Then, it discusses how non-functional requirements (latency, scalability, traceability, and security) influence implementation decisions.

The code snippets presented are illustrative and aim to demonstrate interfaces and responsibilities, not constituting a formal benchmark. The focus is on emphasizing separation of concerns (layout vs. domain), reproducibility (idempotence and caching), and observability (tracing and metrics) as quality criteria for generation services.

Reference Architectures for Document Generation

SVG Image of the Article

Microservices and Rendering Capacity Isolation

In microservice architectures, document generation is often isolated in a dedicated service (document service) (NADAREISHVILI, 2016). This separation reduces coupling between business rules and rendering, prevents domain services from loading layout dependencies, and allows for horizontal scaling of PDF generation independently. Moreover, isolation favors governance of the document lifecycle (versioning, auditing, and retention), as the document service becomes the natural point for applying uniform policies.

The dedicated service can receive synchronous commands (HTTP) when the end user needs the document immediately or asynchronous commands (messaging) when the document is large, during load peaks, or when generation needs to be resilient to temporary failures. Regardless of the invocation mode, it is recommended that the service operates on already normalized and validated DTOs, avoiding direct dependencies on the domain database or cascading calls during rendering.

public class DocumentGenerationService
{
    public async Task<byte[]> GenerateInvoiceAsync(Invoice invoice)
    {
        var document = new InvoiceDocument(invoice);
        return document.GeneratePdf();
    }
}

API Gateway and Data Composition

The API Gateway can act as a facade for the system, centralizing authentication, versioning, rate limiting, and routing (LEVCOVITZ et al., 2016). In document scenarios, it can orchestrate calls to multiple services to build a complete payload (e.g., order data, customer, and payments) before requesting PDF generation. This approach improves the API consumer's experience, but it should be used cautiously to avoid turning the gateway into a complex and hard-to-maintain orchestrator.

An alternative is to shift data composition to an application service (backend-for-frontend or orchestration service) that assembles the DTO and calls the document service. This way, the gateway remains lean, and the flows become more testable. The choice depends on the degree of complexity and the organizational model of responsibilities.

public class DocumentsController : ControllerBase
{
    private readonly DocumentGenerationService _service;

    public DocumentsController(DocumentGenerationService service)
    {
        _service = service;
    }

    [HttpPost("invoice")]
    public async Task<IActionResult> Generate([FromBody] Invoice invoice)
    {
        var pdf = await _service.GenerateInvoiceAsync(invoice);
        return File(pdf, "application/pdf", "invoice.pdf");
    }
}

Messaging and Asynchronous Processing

For documents with high rendering costs (many pages, images, extensive tables) or when generation occurs in batches, asynchronous processing is preferable (STAAR et al., 2018). In this model, the API receives the request and publishes a command/event to a queue (e.g., Azure Service Bus, RabbitMQ, or Kafka). A consumer worker performs PDF generation and persists the artifact in an object repository (Blob Storage, S3) or in the database, returning an identifier for later retrieval. This strategy improves resilience and avoids timeouts in the synchronous path, at the cost of greater consistency and UX complexity (the user needs to wait/check for the result).

An important implication is idempotence: messages may be delivered more than once, and the system must handle this without producing duplication or inconsistencies (SABBAG FILHO, 2025). Thus, the idempotence key (e.g., the hash of the normalized payload) becomes a central element of the design.

Scalability and Quality Strategies in Production

Separation Between Domain and Layout

An essential practice is to avoid mixing business rules with layout code. The document should receive a ready DTO, that is, a structure with the necessary data already calculated (totals, taxes, display rules). With this, the document class focuses on presentation, while domain/application services remain responsible for validation and calculations. This arrangement tends to reduce side effects, facilitate testing, and decrease the likelihood of divergence between the business state and the rendered document.

In practice, the code is organized by document types (e.g., invoice, report, contract), and each type has a versioned layout. Versioning is relevant when the company needs to regenerate historical documents, respecting the layout in effect at the time of issuance.

Cache and Idempotence

Documents are often requested with the same data (reprint, resend, audit). In these cases, it is efficient to use content-based caching (MERTZ; NUNES, 2017). A common procedure is to compute a hash of the payload (or a key composed of identifiers and layout version) and use this key to locate the previously generated PDF in a fast storage (e.g., Redis) or in an object storage. This reduces computational cost, improves latency, and, in asynchronous scenarios, simplifies the handling of message re-deliveries.

However, the caching strategy requires care with invalidation: changes in domain data or layout must alter the key. It is recommended to explicitly include a layout version field and, when applicable, a "calculation version" field to reflect changes in business rules that affect the document.

Observability and SLOs

In production, document generation must be observable. This involves distributed tracing (correlating the user request to the generation and persistence), structured logs with document identifiers, and metrics such as rendering time, size of the generated file, cache hit/miss rate, and failure rate by type. With OpenTelemetry, it is possible to standardize tracing and metrics and define SLOs (e.g., latency percentiles for synchronous generation or completion time for asynchronous queues).

A frequently overlooked detail is the collection of metrics by document "class." Distinct documents have very different costs (a simple invoice vs. a report with dozens of pages). Without this segmentation, alerts and capacity planning become inaccurate.

Implementation: Example in C# with QuestPDF

The following presents a minimal example of a document for an invoice, focusing on the composition structure. In real scenarios, it is recommended to encapsulate styles (typography, colors, spacing) in a shared layer and use resources for images/logos, ensuring visual consistency across documents.

public class InvoiceDocument : IDocument
{
    private readonly Invoice _invoice;

    public InvoiceDocument(Invoice invoice)
    {
        _invoice = invoice;
    }

    public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

    public void Compose(IDocumentContainer container)
    {
        container.Page(page =>
        {
            page.Margin(40);
            page.Content().Column(column =>
            {
                column.Item().Text($"Customer: {_invoice.CustomerName}").Bold();

                foreach (var item in _invoice.Items)
                    column.Item().Text($"{item.Description} - {item.Quantity} x {item.Price}");

                column.Item().Text($"Total: {_invoice.Total}").Bold();
            });
        });
    }
}

It is noted that the example returns a byte array. In document services, it is common to couple this output to a storage strategy (e.g., persist in blob and return a temporary URL) and, when necessary, attach metadata (identifier, layout version, checksum, and timestamps) for auditing and controlled reprocessing.

Heterogeneous Environment: Counterpoint in Go

In organizations with multiple stacks, non-.NET services can also produce PDFs. The decision to centralize generation in a single service (e.g., .NET with QuestPDF) or allow multiple generators (one per stack) involves trade-offs. Centralizing reduces visual variation and concentrates layout governance; distributing allows team autonomy and avoids technological dependence, but increases the risk of inconsistency between documents and complicates standardization.

The following snippet illustrates, in a simplified manner, a generator in Go. Although functional, it highlights that consistency in layout and typography will require additional discipline, especially when documents need to be equivalent across different stacks.

func GenerateReport(report Report) ([]byte, error) {
    pdf := gopdf.GoPdf{}
    pdf.Start(gopdf.PageSizeA4)
    pdf.AddPage()
    pdf.SetFont("Arial", "", 14)
    pdf.Cell(nil, report.Title)
    pdf.Br(20)
    pdf.Cell(nil, report.Content)
    return pdf.WritePdf()
}

Security, Privacy, and Compliance

Documents often contain sensitive data (PII, financial and contractual information). Therefore, the architecture must consider encryption at rest and in transit, identity-based access control and authorization, temporary URLs (signed URLs) for downloads, and access auditing. In regulatory contexts like LGPD, it is recommended to minimize data in the document, mask where applicable, establish retention policies, and provide deletion/anonymization mechanisms when required by legal obligations.

Another practical consideration is the handling of logs: never log document content or the full payload unnecessarily. Generally, only identifiers, hashes, and essential metadata for diagnosis are logged.

Discussion and Threats to Validity

The proposed approach emphasizes the separation of responsibilities, idempotence, and observability, but there are limitations. First, no experimental evaluation (benchmark) was conducted comparing QuestPDF with HTML-to-PDF alternatives or libraries from other languages. Thus, conclusions about performance should be interpreted as engineering recommendations based on architectural properties and practical experience, not as formal empirical proof.

Moreover, the effectiveness of caching depends on the stability of the payload and the frequency of reprints. In domains with highly volatile data, the cache may have a low hit rate. Finally, the choice between synchronous and asynchronous involves user experience and business requirements: there are cases where the user needs the PDF immediately, which forces optimizations in the synchronous path and increases capacity to handle peaks.

Conclusion

QuestPDF offers a modern and controllable approach to document generation in .NET environments, with relevant advantages for distributed architectures due to the predictability and programmatic control of the layout. By treating documents as an independent capability, ideally through a dedicated service, and combining patterns such as caching, idempotence, and messaging, it is possible to achieve a scalable, resilient, and observable solution for corporate workloads.

As future work, it is recommended to empirically evaluate performance and resource consumption across different document classes, as well as compare persistence strategies (blob vs. database) and layout versioning policies under auditing and reprocessing requirements.

References

  • QUESTPDF. Documentation. Available at: https://www.questpdf.com. Accessed on: Jan. 2026. reference.Description
  • NADAREISHVILI, Irakli et al. Microservice architecture: aligning principles, practices, and culture. "O'Reilly Media, Inc.", 2016. reference.Description
  • LEVCOVITZ, Alessandra; TERRA, Ricardo; VALENTE, Marco Tulio. Towards a technique for extracting microservices from monolithic enterprise systems. arXiv preprint arXiv:1605.03175, 2016. reference.Description
  • STAAR, Peter WJ et al. Corpus conversion service: A machine learning platform to ingest documents at scale. In: Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining. 2018. p. 774-782. reference.Description
  • SABBAG FILHO, Nagib. Idempotência em Sistemas Distribuídos: Garantindo Consistência em APIs e Mensageria. Leaders Tec, v. 2, n. 31, 2025. reference.Description
  • MERTZ, Jhonny; NUNES, Ingrid. Understanding application-level caching in web applications: a comprehensive introduction and survey of state-of-the-art approaches. ACM Computing Surveys (CSUR), vol. 50, no. 6, pp. 1-34, 2017. reference.Description
About the author