structural-engineering-and-design
Designing a Flexible Document Generator with the Builder Pattern in Javascript
Table of Contents
Understanding the Builder Pattern for Document Generation
Building complex objects step by step is a common challenge in software development. The Builder pattern solves this by separating the construction of an object from its final representation. In the context of document generation, this pattern allows you to create documents that vary in structure, format, and content while reusing the same construction logic. Whether you need to generate HTML reports, PDF invoices, Markdown readme files, or plain text summaries, the Builder pattern provides a clean, extensible foundation.
This article walks through a practical JavaScript implementation of a document generator using the Builder pattern. You will learn how to define a builder interface, create concrete builders for different output formats, and orchestrate the building process with a director. We will also explore advanced scenarios such as supporting optional sections, mixed content types, and custom styling.
Why Use the Builder Pattern for Document Generation?
Directly constructing a document object inside a constructor or a factory method can become unwieldy when the document has many optional parts or when you need to support multiple output formats. The Builder pattern offers several advantages:
- Separation of concerns: The construction logic is isolated from the representation, making it easier to change the internal structure of a document without affecting how it is built.
- Reusability: The same builder steps can produce different representations by swapping the concrete builder. For example, generating an HTML version and a Markdown version from the same builder steps.
- Fine-grained control: You can build documents step by step, allowing you to conditionally include sections, set metadata, or add formatting.
- Testability: Each builder can be tested independently, and the director can be tested with mock builders.
These benefits make the Builder pattern especially attractive for document generators used in reporting tools, content management systems, and file export utilities.
Core Components of the Builder Pattern
Before diving into code, let’s review the three key participants in the Builder pattern:
- Builder – an interface that declares the methods for creating the parts of a product. In JavaScript, this can be a base class or an object with methods.
- Concrete Builder – implements the Builder interface and assembles the parts. It also provides a method (often
getResult) to retrieve the final product. - Director – defines the order in which to call the builder methods to construct the product. It is optional but helps encapsulate the construction algorithm.
In our document generator, the product is a string (the document content), but it could also be a structured object or a DOM tree.
Step 1: Defining the Builder Interface
We start by defining a base class that outlines the required methods. Each method corresponds to a section or a construction step. The exact methods depend on the document structure you want to support. For a general-purpose document generator, we might need:
class DocumentBuilder {
addTitle(title) {}
addSection(sectionName, content) {}
addCodeBlock(language, code) {}
addTable(headers, rows) {}
addFooter(text) {}
getResult() {}
}
This interface is deliberately generic. Concrete builders will implement these methods according to the target format (e.g., HTML with <h1> tags, Markdown with # headers).
Step 2: Creating Concrete Builders
Let’s implement two concrete builders: one for Markdown and one for HTML. We’ll keep the examples simple to avoid clutter.
Markdown Builder
class MarkdownBuilder extends DocumentBuilder {
constructor() {
super();
this.lines = [];
}
addTitle(title) {
this.lines.push(`# ${title}`, '');
}
addSection(name, content) {
this.lines.push(`## ${name}`, '', content, '');
}
addCodeBlock(language, code) {
this.lines.push(`\`\`\`${language}`, code, '```', '');
}
addTable(headers, rows) {
const headerLine = `| ${headers.join(' | ')} |`;
const separator = `| ${headers.map(() => '---').join(' | ')} |`;
const bodyLines = rows.map(row => `| ${row.join(' | ')} |`);
this.lines.push(headerLine, separator, ...bodyLines, '');
}
addFooter(text) {
this.lines.push('---', text, '');
}
getResult() {
return this.lines.join('\n');
}
}
HTML Builder
class HTMLBuilder extends DocumentBuilder {
constructor() {
super();
this.parts = [];
}
addTitle(title) {
this.parts.push(`<h1>${title}</h1>`);
}
addSection(name, content) {
this.parts.push(`<h2>${name}</h2>`, `<p>${content}</p>`);
}
addCodeBlock(language, code) {
this.parts.push(`<pre><code class="language-${language}">${code}</code></pre>`);
}
addTable(headers, rows) {
const headerHTML = `<thead><tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr></thead>`;
const bodyHTML = rows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`).join('');
this.parts.push(`<table>${headerHTML}<tbody>${bodyHTML}</tbody></table>`);
}
addFooter(text) {
this.parts.push(`<hr><footer>${text}</footer>`);
}
getResult() {
return `<article>${this.parts.join('')}</article>`;
}
}
Both builders implement the same interface but produce different output. This is the core flexibility of the pattern.
Step 3: Building a Director
The director encapsulates a standard construction sequence. It can have multiple construction methods for different document types. Here is a simple director that builds a documentation page:
class DocumentationDirector {
constructor(builder) {
this.builder = builder;
}
constructStandardDoc(title, sections, footer) {
this.builder.addTitle(title);
sections.forEach(section => {
this.builder.addSection(section.name, section.content);
});
this.builder.addFooter(footer);
}
}
The director does not know which concrete builder it is using. It only relies on the interface.
Step 4: Using the Pattern
Now we can generate documents for different output formats with the same director:
const sections = [
{ name: 'Introduction', content: 'This document explains the Builder pattern.' },
{ name: 'Implementation', content: 'We implement builders for Markdown and HTML.' }
];
const mdBuilder = new MarkdownBuilder();
const htmlBuilder = new HTMLBuilder();
const director = new DocumentationDirector(mdBuilder);
director.constructStandardDoc('Builder Pattern Guide', sections, 'Document generated on 2025-01-01');
console.log(mdBuilder.getResult());
director = new DocumentationDirector(htmlBuilder);
director.constructStandardDoc('Builder Pattern Guide', sections, 'Document generated on 2025-01-01');
console.log(htmlBuilder.getResult());
This code produces a Markdown document and an HTML document with identical content but different formatting. Adding a new output format (e.g., PDF, LaTeX, plain text) requires only writing a new builder class.
Advanced: Supporting Optional and Nested Sections
Real-world documents often have optional parts (e.g., a table of contents, an appendix) or nested sections (a section with subsections). We can extend the builder interface to handle these cases. For instance, we can add methods like beginSubSection and endSubSection, or use a context-based approach. Below is an enhancement that allows the director to call a addToc() method if needed:
class FlexibleDocumentDirector {
constructor(builder) {
this.builder = builder;
}
constructWithToc(title, tocItems, sections, footer) {
this.builder.addTitle(title);
if (tocItems.length > 0) {
this.builder.addTableOfContents(tocItems);
}
sections.forEach(section => {
if (section.subsections) {
this.builder.addSection(section.name, section.subsections.map(s => s.content).join(' '));
} else {
this.builder.addSection(section.name, section.content);
}
});
this.builder.addFooter(footer);
}
}
The concrete builders must implement addTableOfContents accordingly. This keeps the system extensible without breaking existing builders (they can provide a default empty implementation).
Real-World Use Cases
The Builder pattern for document generation is not merely an academic exercise. It is used in:
- Static site generators – transforming Markdown content into multiple output formats (HTML, PDF, AMP).
- Report generators – allowing users to configure sections (charts, tables, summary) and export to various formats.
- API documentation tools – building endpoint documentation from the same source into HTML, Markdown, or PDF.
- Invoice/Receipt systems – generating customer-facing documents in different locales or designs.
For inspiration, you can study the source code of popular libraries like nicedoc or the Asciidoctor.js project, which use similar patterns for document conversion.
Testing the Builder and Director
One of the strengths of this pattern is testability. You can unit test each concrete builder independently by calling its methods and asserting the output. For example, using a testing framework like Jest:
test('MarkdownBuilder adds a title with an h1', () => {
const builder = new MarkdownBuilder();
builder.addTitle('Test');
expect(builder.getResult()).toContain('# Test');
});
test('HTMLBuilder wraps content in article tag', () => {
const builder = new HTMLBuilder();
builder.addTitle('Test');
expect(builder.getResult()).toMatch(/.*<\/article>/);
});
The director can also be tested using a mock builder to verify the order of method calls:
test('Director calls builder methods in correct order', () => {
const mockBuilder = {
addTitle: jest.fn(),
addSection: jest.fn(),
addFooter: jest.fn()
};
const director = new DocumentationDirector(mockBuilder);
director.constructStandardDoc('A', [], 'foot');
expect(mockBuilder.addTitle).toHaveBeenCalledWith('A');
expect(mockBuilder.addFooter).toHaveBeenCalledWith('foot');
});
This level of testability makes the system robust as it grows.
Performance and Scalability Considerations
When generating large documents (hundreds of pages), you may need to be mindful of memory usage. The Builder pattern itself does not impose a performance overhead – the actual cost depends on the implementation of each builder. For large documents, consider:
- Using streaming or incremental output (e.g., Node.js streams).
- Lazy evaluation of content sections.
- Abstracting expensive operations (like code highlighting) behind the builder so they are only executed for formats that need them.
The pattern can be combined with the Flyweight pattern to share common formatting objects (e.g., style definitions) across multiple builders.
Alternatives and When Not to Use the Builder Pattern
While the Builder pattern is powerful, it is not always the right choice. If your documents are simple and do not vary much, a straightforward function or a factory may suffice. Also, if you need to produce a single output format only, the overhead of defining multiple builders might be unnecessary. The Builder pattern shines when you have multiple representations of the same construction process, or when the construction process itself is complex and needs to be reused.
Another alternative is the Template Method pattern, where the base class defines the skeleton of the algorithm and subclasses override steps. However, the Builder pattern gives more flexibility because the director can be completely decoupled from the concrete builders – you can reuse the same director with different builders, which is harder with inheritance.
Conclusion
The Builder pattern is an excellent choice for designing flexible document generators in JavaScript. By separating the construction logic from the final representation, you can effortlessly support multiple output formats, add new sections without altering existing code, and keep each component testable. We have shown a practical implementation with Markdown and HTML builders, along with a director that orchestrates the steps. You can extend this pattern to handle tables, code blocks, nested sections, and more.
To dive deeper, you can explore the official Refactoring Guru article on the Builder pattern or the MDN JavaScript Guide for more language-specific insights. For real-world codebases, check out marked (a Markdown parser) or markdown-to-html to see similar design approaches.
Start implementing your own document builder today, and you will quickly appreciate the maintainability and extensibility it brings to your codebase.