Advanced System Design Tutorial 0/70 lessons ~6 min read Lesson 15

    Creational Design Patterns

    Creational patterns decide who is allowed to say 'new', and what must be true before an object escapes. TechLearningPro's certificate desk got this wrong in two directions in the s

    Course progress0%
    Focus
    15 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Creational Design Patterns

    Advanced System Design & Software Architecture · Lesson 15 · Low-Level Design and Design Patterns

    Lesson Overview

    Creational patterns decide who is allowed to say new, and what must be true before an object escapes. TechLearningPro's certificate desk got this wrong in two directions in the same quarter. The first design called new PdfRenderer() inside the controller, so a switch to HTML for accessibility meant editing the controller, the batch job, and a test that copied the switch. The second design, a week after a patterns workshop, introduced an abstract factory, a factory bean, and a prototype registry for a single PDF template. Changing the honors line on the certificate meant finding which factory owned the default font. Learners received last month's template because a shared prototype was mutated and never copied.

    The repair was smaller than either extreme. A certificate is immutable and built with a builder, because it has a cluster of optional fields and one invariant: a document with no learner name and no course title is not a certificate. A DocumentRenderer is chosen at the composition root, not by a subclass hierarchy, until a second format is actually deployed. The letterhead bytes are copied per render so one learner's name cannot leak into the next file. A process-wide singleton is refused for the renderer. Spring already scopes the bean. FactoryBean stays unused, because a @Bean method can construct the renderer in plain sight.

    This lesson covers Factory Method, the static factory idiom that is not Factory Method, Abstract Factory, Builder, Prototype, and a thread-safe Singleton, with the forces that make each one applicable. It treats Spring's singleton scope, prototype scope, and FactoryBean as neighboring mechanisms that interviews mix up. /learn/design-patterns/factory-method, /learn/design-patterns/builder, /learn/design-patterns/singleton, and /learn/design-patterns/prototype are the pattern-course primers. /learn/design-patterns/di-vs-singleton is the container comparison in short form. Here you implement a document export path and you say, for each pattern, the bug it prevents and the bug it adds.

    Learning Objectives

    By the end of this lesson you will be able to:

    1. Distinguish a static factory method, a simple switch that returns a renderer, GoF Factory Method, and Abstract Factory, and pick one for a given number of products.
    2. Build an immutable Certificate with a builder that rejects an incomplete document, and refuse a builder for a two-field value.
    3. Copy a letterhead per export with a copy constructor or copy method, and explain why Cloneable is a poor default.
    4. Implement a thread-safe singleton with a holder class or an enum, and state why double-checked locking needs volatile.
    5. Explain Spring's singleton bean scope, @Scope("prototype"), and FactoryBean (getObject, &beanName) without using them as synonyms for the GoF patterns.
    6. Choose constructor injection over getInstance() for the exporter, and name the testability consequence.
    7. Identify a shared mutable prototype and a factory of factories in review.
    8. Write the applicability note for abstract factory: matched families of parts, not "we have an interface."

    Prerequisites

    • Lesson 14's rule: a pattern needs a force that is true now, and a consequence you can test.
    • Java 21 records, switch expressions, and the memory model at the level of "a volatile write is visible to a later read." You will not re-derive JSR-133. You will apply it to one field.
    • A Spring @Configuration class. Lesson 19 is dependency injection in depth. This lesson only needs the collision between a hand-rolled singleton and a bean.
    • Lesson 11's habit of putting invariants in constructors. The builder is a long constructor with names. It is not a place to hide a broken invariant.

    Detailed Core Concepts

    What is unstable about construction

    new Concrete names a class. That is the right call when the class is the concept, as Money is. It is the wrong call when the caller is the use case and the concrete class is a detail that varies: PDF versus HTML, or a letterhead that must not be shared mutably. Creational patterns move the new behind a name that expresses the rule. They do not remove the need for a rule.

    Keep the vocabulary straight. Interviews collapse it.

    NameWho decides the classTypical Java shapeWhen it is enough
    ConstructorThe callernew Certificate(...)Few arguments, invariant in one place
    Static factory idiom (Bloch)The type itselfCertificate.issue(...), List.ofNamed construction, caching, a subtype hidden behind the return type
    Simple factoryA method with a branchRenderers.forFormat("PDF")A closed switch, one place to edit
    Factory Method (GoF)A subclass of the creatorAbstract renderer() overridden per jobThe creator hierarchy is real and each subclass owns a product
    Abstract Factory (GoF)A family objectParts.header() and Parts.body() from one kitProducts must match; mixing PDF header with HTML body is a bug
    BuilderA staged assemblerCertificate.builder()...build()Many optional fields, one validation at the end
    PrototypeAn existing instancetemplate.copy()Copying is cheaper or safer than rebuilding from parts
    Singleton (GoF)A type with one instanceEnum or holderA true process-wide constraint, not "we have one bean"

    Spring's default bean scope is also called singleton. It means one instance per container, created and wired by the container, replaceable in a test by a different container. The GoF singleton means one instance per class loader, reached through a global point, hard to replace. Same adjective, different consequence. This lesson uses "container singleton" and "GoF singleton" so the collision stays visible.

    Factory Method versus a method that returns an object

    GoF Factory Method: a creator defines an operation that returns a product, and a subclass supplies the class. The point is that the creator's other code (the export steps) stays closed while the product class varies. If you have no creator hierarchy, you do not have this pattern. You have a method.

    java
    public abstract class ExportJob {
    public final byte[] export(Certificate certificate) {
    return renderer().render(certificate);
    }
    protected abstract DocumentRenderer renderer();
    }

    PdfExportJob and HtmlExportJob override renderer. That structure pays when jobs themselves differ — PDF jobs archive to one bucket, HTML jobs inline a stylesheet — and the shared export skeleton must not grow a switch. When both jobs only differ by the renderer, inject DocumentRenderer and delete the hierarchy. Spring @Bean methods are the modern simple factory. They are not Factory Method unless a subclass is the extension point. @ConditionalOnProperty selecting a @Bean is configuration, and it is usually the better extension point for an application, because the variant is deployment, not a subclass the domain must carry.

    Abstract Factory is the family version. A DocumentKit that can create a Header and a Body which are guaranteed to be the same format prevents PdfHeader beside HtmlBody. The force is a real mixing bug. Two methods on one DocumentRenderer (render only) do not need a kit of factories. Introduce the kit when a second part appears and a reviewer can show a defect from mixing families. Until then the kit is the factory of factories lesson 14 deleted.

    Builder

    A builder accumulates arguments and emits one object from build(), which is the only method that may return the product. Use it when a telescoping constructor has become dishonest: many overloads, several nulls at the call site, and an invariant that needs all of the fields at once. A certificate has a required learner name and course title, and optional honors, locale, issue date, and QR payload. Callers should read as names, not as a run of nulls.

    Do not use a builder for VehicleNumber or Money. Those are records with compact constructors. A builder for two fields is a ceremony that hides the type. Do not reuse a builder across threads. It is a mutable accumulator. Do not call build() twice and expect two independent mutable products if the builder retained a reference. The certificate in this lesson is immutable, so a second build() may return an equal instance. Document that. A builder that returns the same mutable buffer twice is a prototype bug wearing another name.

    build() checks the invariant. Setters on the product that "finish initialization" split the check and invite a half-built certificate to be rendered. The product's constructor stays private. The builder lives as a static nested class so it can call that constructor.

    Prototype and why Cloneable fights you

    Prototype creates a new object by copying one that already exists. The force in export is a letterhead: fonts and a logo loaded once from a classpath resource, then specialized with a learner's name. If renderers share one Letterhead and one of them writes the name into the byte buffer, the next learner's PDF contains the previous name. The copy is the fix. The copy is a method you write:

    java
    public Letterhead copy() {
    return new Letterhead(bytes.clone(), locale);
    }

    Cloneable is a marker interface. Object.clone() is protected, performs a shallow field copy, and throws the checked CloneNotSupportedException unless the marker is present. It does not call constructors, so invariants in constructors are skipped. Final fields and deep graphs are awkward. Bloch's guidance, which this course follows, is a copy constructor or a copy method. Use Cloneable only to match an API you do not control.

    Spring @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) is not this pattern. The container calls the bean factory method again and returns a new instance. It does not clone a warmed template. If your letterhead is expensive, a prototype-scoped bean that re-reads the logo from disk on every injection is the opposite of the GoF intent. Hold one template bean (container singleton, immutable or only copied under a method) and call copy() per export.

    Singleton, threads, and the container

    A GoF singleton guarantees a single instance and a global access point. The force is rare in a Spring application: a constraint outside the container, such as a library that must be initialized once per process because the vendor says so, or a cache key space that is process-global by documentation. A stateless renderer is not that force. A @Bean method is.

    When you do need the GoF object, it must be safe to publish under the Java memory model.

    Eager static final. private static final Holder INSTANCE = new Holder(); is safe. Class initialization publishes the object correctly. It constructs at class-load time, which is fine when the cost is small and the class is only loaded if used. It is painful when construction touches the network and tests load the class by accident.

    Initialization-on-demand holder. A nested class holds the instance. The nested class is initialized when getInstance first touches it. The Java Language Specification locks class initialization, so this is safe without an extra synchronized in your method. This is the idiom to prefer for a lazy plain class.

    Enum. enum Exporter { INSTANCE; } is eager, safe, and serialization-safe: the serialization spec returns the existing constant rather than a new object. Prefer this when the singleton is truly one behavior object. It is awkward when the type must extend a class.

    Double-checked locking. Legal from Java 5 onward only if the field is volatile. A non-volatile field can publish a reference while the constructor's writes are still invisible to the reader. The pattern is easy to get wrong and unnecessary given the holder idiom. This lesson shows a correct volatile version so you can recognize it, and recommends the holder or the enum for new code.

    All four break Spring tests if they hold mutable request data. Two @SpringBootTest contexts in one JVM share the static instance. That is the consequence that should push you back to a bean.

    FactoryBean, briefly and with the trap

    org.springframework.beans.factory.FactoryBean<T> is a container SPI:

    • T getObject() produces the object the container exposes.
    • Class<?> getObjectType() tells the container what was produced.
    • isSingleton() defaults to true. When it returns true, the container caches the product.

    If the bean is named renderer, context.getBean("renderer") returns T, and context.getBean("&renderer") returns the FactoryBean itself. Injection by type T receives the product. Injection by FactoryBean is where people accidentally depend on the factory. Do not implement this interface to wrap new HtmlRenderer(). A @Bean method is easier to read and does not introduce the & name. Implement FactoryBean when you are integrating a library whose construction is a callback the container must own, and even then keep it out of the domain package.

    A second trap: isSingleton() returns true while getObject() returns a new object that captures shared mutable state anyway, or the reverse, isSingleton() returns false and a caller caches the result and believes it has a private copy. Match the flag to the cache behavior, and prefer not to enter the SPI.

    Architecture and Design Diagrams

    The export path that this lesson accepts:

    mermaid
    classDiagram
    class Certificate {
    -String learnerName
    -String courseTitle
    -String honors
    +builder() Builder
    }
    class Builder {
    +learnerName(String) Builder
    +courseTitle(String) Builder
    +honors(String) Builder
    +build() Certificate
    }
    class Letterhead {
    -byte[] bytes
    +copy() Letterhead
    }
    class DocumentRenderer {
    <<interface>>
    +render(Certificate) byte[]
    }
    class PdfRenderer
    class HtmlRenderer
    class CertificateExporter {
    +export(Certificate) byte[]
    }
    Certificate +-- Builder
    CertificateExporter --> DocumentRenderer
    DocumentRenderer <|.. PdfRenderer
    DocumentRenderer <|.. HtmlRenderer
    PdfRenderer --> Letterhead
    HtmlRenderer --> Letterhead

    There is no ExportJob hierarchy and no DocumentKit. The diagram would grow a kit only when header and body can be mixed across formats. CertificateExporter depends on the interface. The @Bean method is the simple factory and lives in configuration, off this domain picture, which is lesson 12's rule about not drawing the container as if it were the model.

    The copy happens on the render sequence, once per call, not when the template bean starts.

    mermaid
    sequenceDiagram
    participant Job as CertificateExporter
    participant Template as Letterhead
    participant Copy as Letterhead copy
    participant Renderer as DocumentRenderer
    Job->>Template: copy()
    Template-->>Job: Copy
    Job->>Renderer: render(certificate, copy)
    Note over Renderer,Copy: writes the learner into the copy only
    Renderer-->>Job: bytes

    A sequence that omits copy() and passes Template into a renderer that writes is the bug from the overview. The object diagram of the failure is two exports whose byte arrays compare equal in the name region. The test in the walkthrough locks that.

    Technical Explanation

    Immutability is part of creation

    Certificate has private final fields. build() copies any array it is given. Getters do not return internal mutable arrays. A renderer that needs to stamp a name does it on the letterhead copy, not by calling a setter on the certificate. Creational patterns wrapped around a mutable product with setters undo the invariant. If the export must be edited later, that is a new certificate or a separate draft type with its own lifecycle, not a hole in build().

    Thread safety of each pattern

    • Builder. Confined to the thread that fills it. Do not store a builder in a bean field.
    • Immutable certificate and an unshared letterhead copy. Safe to hand to another thread after publication. Safe publication is the return from build() or copy() through a final field or a volatile write. Returning a private array without copying is not publication of a private snapshot; it is sharing.
    • GoF singleton. The holder or enum publishes the instance safely. The work it does must still be safe. A singleton AtomicLong for export sequence numbers is coherent. A singleton ArrayList of in-flight exports is a race. Synchronization belongs on the mutable state, or the state should not be in the singleton.
    • Simple factory method. Renderers.forFormat must not stash the last renderer in a static field. Stateless selection is safe. A static lastFormat used as a cache is a race and a cross-request leak.

    Failure and partial construction

    build() throws IllegalArgumentException before any file is written. render may throw after the copy exists and before bytes are returned. The exporter does not catch that and return an empty array. An empty PDF that looks successful is worse than an error. The controller maps the exception to a 422 for a bad certificate and a 502 for a renderer failure if the renderer called out, which this in-process renderer does not. Keep construction failures and rendering failures as different types if the HTTP mapping needs to differ. A single ExportException that hides both makes the sequence's alt impossible.

    Security

    The QR payload and the learner name are data, not file paths. The renderer must not concatenate the name into a classpath location or a shell. Letterhead bytes come from a resource the application owns, classpath:letterhead/campus.bin, loaded by Spring's ResourceLoader or ClassPathResource, not from a request parameter. A builder method templatePath(String) that accepts the client path is an arbitrary-file read. The pattern note should say the template is server-selected. Honor lines are plain text. HTML rendering escapes them. PDF rendering uses an API that draws text rather than interpreting the honors line as a document script. The builder does not make that safe by itself. The renderer does, and the builder's job is to refuse a blank name so you are not escaping an empty document into a signed-looking file.

    Practical Implementation or Design Walkthrough

    Problem

    Export a course certificate as PDF or HTML. Required: learner name, course title. Optional: honors, locale, issue instant, QR payload. One letterhead is loaded at startup. Renders must not leak names across learners. One format is live in production (PDF). HTML is scheduled for the accessibility milestone next month, so the interface exists and the second class is stubbed behind the same contract. There is no family of headers and footers yet.

    The certificate and its builder

    java
    package com.techlearningpro.certs;
    import java.time.Instant;
    import java.util.Objects;
    public final class Certificate {
    private final String learnerName;
    private final String courseTitle;
    private final String honors;
    private final String locale;
    private final Instant issuedAt;
    private final String qrPayload;
    private Certificate(String learnerName, String courseTitle, String honors,
    String locale, Instant issuedAt, String qrPayload) {
    this.learnerName = learnerName;
    this.courseTitle = courseTitle;
    this.honors = honors;
    this.locale = locale;
    this.issuedAt = issuedAt;
    this.qrPayload = qrPayload;
    }
    public static Builder builder() {
    return new Builder();
    }
    public String learnerName() { return learnerName; }
    public String courseTitle() { return courseTitle; }
    public String honors() { return honors; }
    public String locale() { return locale; }
    public Instant issuedAt() { return issuedAt; }
    public String qrPayload() { return qrPayload; }
    public static final class Builder {
    private String learnerName;
    private String courseTitle;
    private String honors = "";
    private String locale = "en";
    private Instant issuedAt;
    private String qrPayload = "";
    public Builder learnerName(String learnerName) {
    this.learnerName = learnerName;
    return this;
    }
    public Builder courseTitle(String courseTitle) {
    this.courseTitle = courseTitle;
    return this;
    }
    public Builder honors(String honors) {
    this.honors = honors == null ? "" : honors;
    return this;
    }
    public Builder locale(String locale) {
    this.locale = locale;
    return this;
    }
    public Builder issuedAt(Instant issuedAt) {
    this.issuedAt = issuedAt;
    return this;
    }
    public Builder qrPayload(String qrPayload) {
    this.qrPayload = qrPayload == null ? "" : qrPayload;
    return this;
    }
    public Certificate build() {
    if (learnerName == null || learnerName.isBlank()) {
    throw new IllegalArgumentException("learnerName");
    }
    if (courseTitle == null || courseTitle.isBlank()) {
    throw new IllegalArgumentException("courseTitle");
    }
    if (locale == null || locale.isBlank()) {
    throw new IllegalArgumentException("locale");
    }
    Instant when = issuedAt == null ? Instant.EPOCH : issuedAt;
    return new Certificate(learnerName.trim(), courseTitle.trim(), honors, locale, when, qrPayload);
    }
    }
    }

    Instant.EPOCH as a default issued-at is a visible placeholder only if the caller forgot. Prefer requiring issuedAt when the legal document must show a real instant. The walkthrough requires it in the test by passing Clock.systemUTC().instant() from the exporter, not from inside build(), so time stays injectable. Adjust build() to reject a null issuedAt if the product is a legal record. The sample rejects blank names and titles, which is the invariant that has already caused a blank "certificate" to be mailed.

    Letterhead prototype

    java
    package com.techlearningpro.certs;
    import java.util.Arrays;
    import java.util.Objects;
    public final class Letterhead {
    private final byte[] bytes;
    private final String locale;
    public Letterhead(byte[] bytes, String locale) {
    this.bytes = Arrays.copyOf(bytes, bytes.length);
    this.locale = Objects.requireNonNull(locale);
    }
    public Letterhead copy() {
    return new Letterhead(bytes, locale);
    }
    public byte[] bytes() {
    return Arrays.copyOf(bytes, bytes.length);
    }
    public String locale() { return locale; }
    }

    copy() goes through the constructor, so the new object has its own array. bytes() returns a copy so a renderer cannot alias the template. That is two copies on the read path. For a small logo this is the correct trade. For a multi-megabyte asset, return a read-only view you control, or pass a bounded stream, and still never hand out the internal array. Do not implement Cloneable.

    Renderer chosen in configuration

    java
    package com.techlearningpro.certs;
    import java.util.Arrays;
    public interface DocumentRenderer {
    byte[] render(Certificate certificate, Letterhead letterhead);
    }
    public final class PdfRenderer implements DocumentRenderer {
    @Override
    public byte[] render(Certificate certificate, Letterhead letterhead) {
    byte[] canvas = letterhead.bytes();
    // A real exporter writes PDF operators with a library such as OpenPDF or PDFBox.
    // This lesson's contract is the isolation of the learner name to this canvas.
    return stamp(canvas, certificate.learnerName());
    }
    static byte[] stamp(byte[] canvas, String learnerName) {
    byte[] name = learnerName.getBytes(java.nio.charset.StandardCharsets.UTF_8);
    byte[] out = Arrays.copyOf(canvas, canvas.length + name.length);
    System.arraycopy(name, 0, out, canvas.length, name.length);
    return out;
    }
    }

    The stamp is deliberately naive so the test can show isolation without a PDF library. Production replaces stamp with PDFBox's APIs (org.apache.pdfbox.pdmodel.PDDocument), which are real and out of scope for the pattern. The pattern does not depend on a fake PdfLibrary.make(). HTML is a second class, HtmlRenderer, that escapes the name and is selected by a bean. The simple factory is configuration:

    java
    @Configuration
    class CertBeans {
    @Bean
    Letterhead letterhead(org.springframework.core.io.ResourceLoader loader) throws java.io.IOException {
    var resource = loader.getResource("classpath:letterhead/campus.bin");
    try (var in = resource.getInputStream()) {
    return new Letterhead(in.readAllBytes(), "en");
    }
    }
    @Bean
    DocumentRenderer renderer() {
    return new PdfRenderer();
    }
    @Bean
    CertificateExporter exporter(DocumentRenderer renderer, Letterhead letterhead) {
    return new CertificateExporter(renderer, letterhead);
    }
    }

    ResourceLoader and ClassPathResource behavior (classpath:) are Spring's. getInputStream throws IOException. There is no FactoryBean.

    java
    public final class CertificateExporter {
    private final DocumentRenderer renderer;
    private final Letterhead template;
    public CertificateExporter(DocumentRenderer renderer, Letterhead template) {
    this.renderer = renderer;
    this.template = template;
    }
    public byte[] export(Certificate certificate) {
    return renderer.render(certificate, template.copy());
    }
    }

    The test that would have caught the shared buffer

    java
    @Test
    void exportsDoNotShareLetterheadBytes() {
    Letterhead template = new Letterhead(new byte[] {1, 2, 3}, "en");
    DocumentRenderer renderer = new PdfRenderer();
    CertificateExporter exporter = new CertificateExporter(renderer, template);
    Certificate first = Certificate.builder().learnerName("Asha").courseTitle("Java").issuedAt(Instant.EPOCH).build();
    Certificate second = Certificate.builder().learnerName("Ben").courseTitle("Java").issuedAt(Instant.EPOCH).build();
    byte[] a = exporter.export(first);
    byte[] b = exporter.export(second);
    assertFalse(Arrays.equals(a, b));
    assertArrayEquals(new byte[] {1, 2, 3}, template.bytes());
    }

    If export forgets copy() and PdfRenderer writes into the array from letterhead.bytes() when that method returned the internal buffer, the template assertion fails. The test is the prototype's consequence.

    A thread-safe singleton, and where it does not go

    An export-sequence counter that must be process-wide because a legacy archive names files cert-<n>.pdf and the vendor forbids gaps across nodes is a poor singleton: two pods will collide. A counter inside one process, documented as best-effort and not an archive key, can be:

    java
    public final class LocalExportCounter {
    private LocalExportCounter() {}
    private static final class Holder {
    static final LocalExportCounter INSTANCE = new LocalExportCounter();
    }
    private final java.util.concurrent.atomic.AtomicLong next = new java.util.concurrent.atomic.AtomicLong();
    public static LocalExportCounter getInstance() {
    return Holder.INSTANCE;
    }
    public long next() {
    return next.incrementAndGet();
    }
    }

    The holder class is initialized under the class-initialization lock. AtomicLong makes the increment safe. This is still a global. Tests that need a fixed id should not call it. The certificate's identity remains OrderId or a UUID from the caller. The counter is here as the idiom, not as the recommended id source. The enum form is the alternative when you want serialization safety:

    java
    public enum LocalExportIds {
    INSTANCE;
    private final java.util.concurrent.atomic.AtomicLong next = new java.util.concurrent.atomic.AtomicLong();
    public long next() { return next.incrementAndGet(); }
    }

    Double-checked locking, if you meet it in a review, must look like this or be rewritten as a holder. The field is volatile. Both reads of instance in a correct version re-read after the lock, or use a local variable carefully. The holder is shorter and harder to break. A non-volatile DCL field is a defect even if tests pass on a lightly loaded laptop.

    What was refused

    Abstract factory: no second part to keep consistent with the header. Factory Method hierarchy: no divergent job skeleton. FactoryBean: the @Bean methods are readable. GoF singleton for PdfRenderer: the container scope is enough, and a static renderer would share a letterhead by accident. Cloneable: the copy method calls the constructor.

    Enterprise Real-World Scenarios

    The template that remembered the previous learner

    Certificate export kept one byte[] on a singleton renderer. The first implementation copied on the way in and wrote on that array during layout, then stored the array back on the bean "to reuse the buffer." Load testing with two threads produced PDFs whose visible names alternated. The defect was a prototype that was not a copy, plus a GoF-style singleton that made the buffer process-wide. The fix was Letterhead.copy() per export, a container-scoped immutable template, and the test above. The team also dropped a FactoryBean<DocumentRenderer> whose getObject returned the same renderer and whose isSingleton returned false, which had caused Spring to request a new product while the factory handed out one shared mutable instance. The & bean name showed up in a test injection and nobody knew why the factory itself was in the graph. Deleting the FactoryBean removed the ambiguity.

    HTML was a switch in three services

    When accessibility asked for HTML, the PDF new was copied in the web tier, the email attachment job, and an admin reissue tool. One of them ignored honors. The simple factory was a @Bean of DocumentRenderer selected by app.certs.format. All three callers already depended on CertificateExporter, so one wiring change covered them. A Factory Method hierarchy was proposed so each channel could subclass ExportJob. The channels did not differ in steps, only in who invoked them. The hierarchy was rejected in the note: reopen if the email job must stamp a different archive footer inside the shared skeleton. Until then, injection is the creational design. Abstract factory was rejected in the same note, with the reopen condition "a header widget and a body widget that can be mixed across formats."

    Best Practices

    • Put the invariant in build() or the constructor, not in the caller.
    • Use a builder when names and optional fields make the constructor a run of nulls. Use a record when they do not.
    • Prefer a @Bean method over Factory Method, Abstract Factory, and FactoryBean until a hierarchy or a product family is the actual change.
    • Copy prototypes with a method that goes through a constructor. Copy arrays on the way in and the way out.
    • Do not use Cloneable for new types.
    • Prefer holder or enum for a GoF singleton you still need. Make volatile mandatory if you keep double-checked locking. Prefer deleting the singleton.
    • Do not store request data in a static instance or in a container singleton.
    • Keep builders off shared fields. They are not thread-safe.
    • Select the letterhead on the server. Do not let the client pass a template path.
    • Distinguish container singleton scope from the GoF singleton in every review comment that uses the word.
    • Distinguish @Scope("prototype") from copy().
    • Test that two exports cannot observe each other's mutable buffers, and that a blank name never becomes bytes.

    Common Mistakes and Anti-Patterns

    Factory Method in name only. A class called CertificateFactory with a static method and no subclass. Call it a static factory. The GoF pattern is a subclass hook. The wrong name sends the next reader looking for a hierarchy to extend.

    Abstract factory for one product. DocumentKit with a single renderer() method. You added types and still have one path. A DocumentRenderer bean is the design.

    Builder that does not build a valid object. build() allows a blank name, and the renderer "handles it." Blank certificates get mailed. The check belongs in build().

    Shared prototype. One letterhead buffer, mutated, held by a singleton. Names leak across learners. Copy per use, and do not write back to the template.

    Cloneable shallow copy. The copy shares a nested array. The leak remains, with more code. A copy method that copies the bytes is shorter.

    Non-volatile double-checked locking. A reference published before construction is visible. Under load, a caller sees a half-initialized counter or renderer. Replace it with the holder idiom.

    GoF singleton as a @Service habit. getInstance() inside a bean, plus container wiring. Tests share state, and two contexts corrupt each other. Inject the collaborator.

    FactoryBean for new. The & name and isSingleton become a second design. A @Bean method is the simple factory the container already understands.

    Client-supplied template path. The builder accepts a path, and the exporter reads it. That is an arbitrary file read. The template is a server resource.

    Troubleshooting and Design Review

    Symptom: Two certificates in one batch contain the wrong learner name, and only under load.

    Likely cause: a shared mutable buffer on a singleton renderer or a letterhead that copy() does not deep-copy.

    Investigation: identity of the byte[] before and after render. Look for a field on a @Bean that is written during render.

    Corrective action: copy() through the constructor, Arrays.copyOf on every escape, the test from this lesson. Remove static caches of the last export.

    Symptom: Tests pass alone and fail when the suite runs, with leftover sequence numbers or templates.

    Likely cause: a GoF singleton.

    Investigation: static fields and getInstance in the cert package.

    Corrective action: container scope and constructor injection. If a process-wide counter is a real vendor constraint, isolate it and reset it in a documented test hook rather than hiding it.

    Symptom: Injection of the renderer receives a factory, or a second bean named &renderer appears in the graph.

    Likely cause: a FactoryBean where a @Bean method was enough, and a caller injected the factory type.

    Investigation: implements FactoryBean, and isSingleton versus what getObject returns.

    Corrective action: delete the FactoryBean. Construct the product in @Bean.

    Symptom: Adding HTML means editing the controller and the batch job.

    Likely cause: new PdfRenderer() at each call site. The simple factory was never the only new.

    Investigation: references to the concrete class outside configuration.

    Corrective action: depend on DocumentRenderer or CertificateExporter. Select the class in one @Bean.

    Symptom: A blank name produces a file that downstream treats as issued.

    Likely cause: build() does not check, or a second constructor bypasses the builder.

    Investigation: constructors of Certificate. They should be private.

    Corrective action: one build() path, a test that blank input throws, no setters.

    Review questions: Who calls new on the concrete renderer? Does build() reject an illegal certificate? Does each export get a distinct letterhead array? Is this singleton container scope or a static instance? Are we about to add an abstract factory for a family that has one member? Does FactoryBean do anything a @Bean method cannot?

    Interview Questions and Answers

    Q1. How do you tell a static factory from Factory Method from Abstract Factory?

    A static factory is a method on the concrete type, such as Certificate.builder or List.of. There is no subclass deciding the product. Factory Method is an overridable creator method so a subclass picks the class while a superclass keeps the algorithm, as in ExportJob.renderer(). Abstract Factory builds a family of parts that must match, such as a header and a body from the same format. I use a static factory or a @Bean method unless I can point at the hierarchy or the mixing bug. Most "factory" answers in interviews are static factories, and I say so before I draw a hierarchy I do not need.

    Q2. When is a builder the wrong tool?

    When the type has a handful of required fields and a compact constructor can check them, as Money and VehicleNumber do. A builder for those types adds a mutable halfway state and a second API. I use a builder for the certificate because honors, locale, and QR payload are optional and the call site should name them, and because build() is the single gate for "name and title required." I do not share builders across threads.

    Q3. Why do you avoid Cloneable for the letterhead?

    Object.clone is protected, shallow, and skips constructors, so a copied letterhead can still share the byte array and can bypass the copy I put in the constructor. A copy() method that calls new Letterhead(bytes, locale) runs that constructor and gives the new instance its own array. I implement Cloneable only to satisfy an API I do not control. The test that two exports leave the template bytes unchanged is what I defend, not the interface list.

    Q4. How do you make a singleton thread-safe, and when do you refuse to?

    Holder class or enum, so class initialization publishes the instance, plus AtomicLong or another concurrent type for the mutable part. Double-checked locking only with a volatile field, and I still prefer the holder. I refuse a singleton for the renderer and for anything request-scoped. Spring's singleton scope is one bean per container and is replaceable in tests. A static getInstance is shared by every context in the JVM. That consequence is usually unacceptable for application code.

    Q5. What is the bug in double-checked locking without volatile?

    A thread can see the reference to the instance while the constructor's writes are not yet visible, and use a half-initialized object. volatile on the field, from Java 5 on, prevents that publication bug. It does not make a mutable singleton's methods safe. I still replace the block with a holder so the next edit cannot drop the modifier.

    Q6. What does Spring's FactoryBean actually return?

    getObject()'s product is what you get from the bean name and from injection of T. The factory itself is the name prefixed with &. isSingleton() tells the container whether to cache the product. I do not use it to construct a renderer I can new in a @Bean method. I have seen isSingleton false paired with a factory that returned one shared mutable object, which recreated the letterhead leak and confused injection. The @Bean method has neither trap.

    Q7. How is @Scope("prototype") different from the Prototype pattern?

    Prototype scope asks the container to call the creation method again, producing a fresh bean, often re-running expensive setup. The GoF prototype copies an instance you already warmed, which is what Letterhead.copy() does with the logo bytes. I use a container singleton for the template and a copy per export. I do not mark the letterhead prototype-scoped and re-read the file on every injection.

    Q8. Where does dependency injection sit relative to these patterns?

    It replaces most of the reasons people used to reach for a singleton or a service locator. The exporter receives DocumentRenderer and Letterhead in its constructor. Tests pass fakes. The composition root is @Configuration. I still use builders and copy methods inside the graph the container built. Injection does not build a certificate from twelve optional fields, and a container does not deep-copy a letterhead unless I wrote copy(). Lesson 19 takes the container further. The boundary is: the container wires long-lived collaborators; creational patterns inside the domain protect invariants and copies.

    Q9. What test do you want for a builder and for a prototype?

    For the builder, blank name and blank title throw, and a built certificate exposes no setters. For the prototype, two exports with different names produce different bytes and the template's own bytes are unchanged. I also want a test that the concrete PDF class is not referenced from the controller. If that test is an ArchUnit rule or a review checklist, I say which. I do not test that a class "implements the pattern."

    Q10. A reviewer wants an abstract factory because HTML is next month. What do you write in the decision?

    I write that one DocumentRenderer bean, selected in configuration, covers PDF now and HTML when the class exists. Abstract factory waits until two parts must be kept in a family, with an example of the mixing bug we refuse. Factory Method waits until export jobs share a skeleton and diverge inside a step. The reopen conditions are those bugs, not the calendar date alone. Introducing the kit today would make the honors-line change cross another type for no mixing defect we have.

    Q11. How do you keep construction failures from looking like a successful empty file?

    build() throws before render. render throws if it cannot produce bytes, and the exporter does not catch that and return a zero-length array. The controller maps validation to 422 and an unexpected renderer failure to 500. I keep those as separate outcomes on the sequence. A creational pattern that swallows IllegalArgumentException and returns new byte[0] has failed its only job.

    Hands-On Exercises

    Exercise: Certificate export

    Problem statement. Implement certificate export with a builder, a copied letterhead, and a renderer chosen in configuration. PDF is live. Do not add an abstract factory. Do not use FactoryBean or Cloneable. Do not store the letterhead in a GoF singleton.

    Tasks.

    1. Implement Certificate.Builder so a blank learner name or course title throws, and the product has no setters.
    2. Implement Letterhead.copy() with an independent byte array.
    3. Implement CertificateExporter.export so each call renders against a copy.
    4. Wire DocumentRenderer and Letterhead with @Bean, loading bytes from a classpath resource.
    5. Write the two-export test that the template is unchanged and the outputs differ.
    6. Show a correct holder-class singleton only for a process-local counter, and a sentence on why the renderer must not use it.
    7. Write the refusal note for abstract factory and for FactoryBean, with reopen conditions.
    8. State how HTML would be added without editing CertificateExporter.

    Expected deliverables. The Java types, the configuration, the isolation test, and the two refusal notes.

    Evaluation rubric.

    Assessment areaWeightWhat "good" looks like
    Builder invariant20%Illegal certificates cannot be built
    Prototype isolation25%Template bytes survive two exports
    Selection15%Concrete renderer is named in configuration only
    Container versus GoF15%No static renderer; holder idiom only where justified
    API honesty15%No Cloneable, no FactoryBean, no client template path
    Judgment10%Abstract factory refused with a real reopen condition

    A hierarchy of export jobs for one render call misses the exercise, even if every class has a pattern name in its suffix.

    Lesson Summary

    Creational design is the rule for new: who chooses the class, when the invariant runs, and whether the next caller sees a fresh copy. Static factories and @Bean methods cover most application choices. Factory Method and Abstract Factory wait for a real hierarchy or a real product family. A builder is a named, validating constructor for a fat immutable document. Prototype is a copy method, not Cloneable and not Spring's prototype scope. A thread-safe GoF singleton is a holder or an enum, and it is usually the wrong tool next to a container that already scopes beans. FactoryBean is a container SPI with an & name and a cache flag, not a badge of a careful design. The certificate path that survived review builds a valid document, copies the letterhead, and renders through an interface the batch job and the controller already share. Lesson 16 composes objects that already exist: adapters, decorators, facades, proxies, and trees of parts.

    Further Reading


    Ready to mark this lesson complete?Track your journey across the entire course.