> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Implement Factory and Abstract Factory in Java 21
- URL: https://www.ggorantala.dev/how-to-implement-factory-abstract-factory-pattern-java/
- Published: 2026-06-30T04:54:00.000Z
- Updated: 2026-10-31T06:35:01.000Z
- Description: Build a URL shortener in Java 21 with a static factory, a registry factory and an abstract factory, then swap memory for file storage with one config value.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, design-patterns, java-tutorials

## What the shortener prints when it's done

Run the same program three times with different arguments and you get three different code generators and two different storage back ends, without a single `if` in the service that does the work:

```log
generator=sequential storage=memory
G9 -> https://ggorantala.dev/how-to-use-java-records/
GA -> https://ggorantala.dev/how-to-use-optional-java/
clicks G9 = 2
clicks GA = 1
unknown code -> Optional.empty

```

```log
generator=hash storage=file
1CMqNAHRO -> https://ggorantala.dev/how-to-use-java-records/
1DQhMUpnP -> https://ggorantala.dev/how-to-use-optional-java/
clicks 1CMqNAHRO = 2
clicks 1DQhMUpnP = 1
unknown code -> Optional.empty

```

That's the factory pattern in Java doing its job: the caller says *what* it wants (`"hash"`, `"file"`) and a factory decides *which class* to build. We'll build a small URL shortener and use three flavours of the pattern on it: a **static factory method**, a **registry factory** that picks a class by name, and an **abstract factory** that produces a matching family of objects (a link store and a click log that must agree on where data lives).

This is for you if you've seen `new` scattered through your code and want to know when pulling it into a factory pays off, and when it's just ceremony. You need to be comfortable with interfaces and records. Everything runs on a bare JDK, so there's nothing to install beyond Java.

```log
Main ──> CodeGenerators.create("hash") ──> CodeGenerator   (factory with registry)
  │
  └───> StorageFactory.fromConfig(...) ──> StorageFactory   (abstract factory)
                                             ├─ linkStore() ──> LinkStore
                                             └─ clickLog()  ──> ClickLog
LinkService only sees the interfaces.

```

## Versions and tools you'll want

| Tool                    | Version I used                                         | Check it with  |
| ----------------------- | ------------------------------------------------------ | -------------- |
| JDK                     | OpenJDK 21.0.12.1 (Ubuntu build)                       | java -version  |
| Build                   | plain javac, no Maven or Gradle                        | javac -version |
| IntelliJ IDEA / Eclipse | any current release, only needed if you'd rather click | Help → About   |

No third-party libraries at all. I wanted to use JUnit for the tests, but Maven Central wasn't reachable from the machine I built this on, so the tests are a plain `main` method with a tiny `check()` helper. Section 6 shows how to convert them to JUnit 5 in about two minutes. Java 17 is enough for everything here, since I compiled the main sources with `--release 17` too and they passed.

## Get the shortener project onto disk

Create this tree. Every file is listed in full below, so you can type or paste them in.

```log
shortener/
└── src/
    ├── main/java/dev/ggorantala/shortener/
    │   ├── ShortLink.java
    │   ├── LinkService.java
    │   ├── Main.java
    │   ├── codes/
    │   │   ├── CodeGenerator.java
    │   │   ├── CodeGenerators.java
    │   │   ├── Base62.java
    │   │   ├── SequentialCodeGenerator.java
    │   │   ├── RandomCodeGenerator.java
    │   │   └── HashCodeGenerator.java
    │   └── storage/
    │       ├── LinkStore.java
    │       ├── ClickLog.java
    │       ├── StorageFactory.java
    │       ├── InMemoryStorageFactory.java
    │       └── FileStorageFactory.java
    └── test/java/dev/ggorantala/shortener/Checks.java

```

**From the command line (what I used):**

```bash
mkdir -p shortener/src/main/java/dev/ggorantala/shortener/{codes,storage}
mkdir -p shortener/src/test/java/dev/ggorantala/shortener
cd shortener

```

**In IntelliJ IDEA:** File → New → Project, choose *Java*, pick your JDK 21, build system *IntelliJ*. Then create the packages `dev.ggorantala.shortener`, `dev.ggorantala.shortener.codes` and `dev.ggorantala.shortener.storage` under `src`. I didn't click through this on the machine I built on (no GUI), so treat the menu path as from the JetBrains docs, not executed.

**In Eclipse:** File → New → Java Project, set the execution environment to JavaSE-21, then right-click `src` → New → Package for the three packages. Same caveat: docs, not executed.

Compile and run from the project root on Linux or macOS:

```bash
mkdir -p out
javac -Xlint:all -d out $(find src/main -name '*.java')
java -cp out dev.ggorantala.shortener.Main sequential memory

```

On Windows PowerShell the `$(find ...)` trick doesn't exist. Use `javac -d out (Get-ChildItem -Recurse src/main -Filter *.java).FullName` instead. I haven't run that line; it's the standard PowerShell equivalent.

## How to implement the factory pattern in Java, three factories deep

Three steps, each one a different kind of factory. Build them in order, because each one fixes a problem the previous one leaves behind.

### Step 1: Put validation in a static factory method

**Goal:** make it impossible to hold an invalid `ShortLink`, and give construction a readable name.

`src/main/java/dev/ggorantala/shortener/ShortLink.java`

```java
package dev.ggorantala.shortener;

import java.net.URI;
import java.time.Instant;

/** One shortened link. Constructed only through the static factories. */
public record ShortLink(String code, URI target, Instant createdAt) {

    // Compact constructor: every instance, however created, passes through here.
    public ShortLink {
        if (code == null || !code.matches("[0-9A-Za-z]{1,12}")) {
            throw new IllegalArgumentException("code must be 1-12 letters or digits: " + code);
        }
        if (target == null || target.getScheme() == null
                || !(target.getScheme().equals("http") || target.getScheme().equals("https"))) {
            throw new IllegalArgumentException("target must be an http(s) URI: " + target);
        }
        if (createdAt == null) {
            throw new IllegalArgumentException("createdAt is required");
        }
    }

    /** Named factory: says what it does, unlike new ShortLink(...). */
    public static ShortLink of(String code, String target, Instant createdAt) {
        return new ShortLink(code, URI.create(target), createdAt);
    }

    /** Factory from a stored line: code, tab, target, tab, epoch millis. */
    public static ShortLink parse(String line) {
        String[] parts = line.split("\t");
        if (parts.length != 3) {
            throw new IllegalArgumentException("expected 3 tab-separated fields: " + line);
        }
        return of(parts[0], parts[1], Instant.ofEpochMilli(Long.parseLong(parts[2])));
    }

    public String toLine() {
        return code + "\t" + target + "\t" + createdAt.toEpochMilli();
    }
}

```

A **static factory method** is a plain `static` method that returns an instance: `ShortLink.of(...)` and `ShortLink.parse(...)` above. It's the simplest factory there is and the one I reach for first. `parse` can fail in ways a constructor can't describe, and the name `of` reads better than `new ShortLink(code, URI.create(...), now)` at every call site. The real guarantee comes from the record's compact constructor, though: every route in, including plain `new`, runs the same checks.

**Checkpoint:** nothing to run yet. Move on.

### Step 2: Define what a code generator is, then hide the implementations

**Goal:** callers depend on one small interface and never name a concrete class.

`src/main/java/dev/ggorantala/shortener/codes/CodeGenerator.java`

```java
package dev.ggorantala.shortener.codes;

/** Product interface: anything that can hand out the next short code. */
public interface CodeGenerator {
    String next(String target);
}

```

`src/main/java/dev/ggorantala/shortener/codes/Base62.java`

```java
package dev.ggorantala.shortener.codes;

final class Base62 {
    private static final String ALPHABET =
            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private Base62() { }

    static String encode(long value) {
        if (value == 0) return "0";
        StringBuilder sb = new StringBuilder();
        long v = value;
        while (v > 0) {
            sb.append(ALPHABET.charAt((int) (v % 62)));
            v /= 62;
        }
        return sb.reverse().toString();
    }
}

```

Three implementations. All three are **package-private** (no `public`), which is the whole point: code outside the `codes` package physically can't write `new HashCodeGenerator()`.

`src/main/java/dev/ggorantala/shortener/codes/SequentialCodeGenerator.java`

```java
package dev.ggorantala.shortener.codes;

import java.util.concurrent.atomic.AtomicLong;

/** 1, 2, 3 ... in base 62. Predictable, so fine for tests and internal links. */
final class SequentialCodeGenerator implements CodeGenerator {
    private final AtomicLong counter = new AtomicLong(1000);

    @Override
    public String next(String target) {
        return Base62.encode(counter.incrementAndGet());
    }
}

```

`src/main/java/dev/ggorantala/shortener/codes/RandomCodeGenerator.java`

```java
package dev.ggorantala.shortener.codes;

import java.security.SecureRandom;

/** Seven random base-62 digits. Not guessable, but collisions are possible. */
final class RandomCodeGenerator implements CodeGenerator {
    private final SecureRandom random = new SecureRandom();

    @Override
    public String next(String target) {
        return Base62.encode(Math.abs(random.nextLong() % 3_521_614_606_208L)); // 62^7
    }
}

```

`src/main/java/dev/ggorantala/shortener/codes/HashCodeGenerator.java`

```java
package dev.ggorantala.shortener.codes;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/** Same target, same code. Handy for de-duplicating links. */
final class HashCodeGenerator implements CodeGenerator {
    @Override
    public String next(String target) {
        try {
            byte[] d = MessageDigest.getInstance("SHA-256")
                    .digest(target.getBytes(StandardCharsets.UTF_8));
            long v = 0;
            for (int i = 0; i < 6; i++) v = (v << 8) | (d[i] & 0xFF);
            return Base62.encode(v);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 is required on every JDK", e);
        }
    }
}

```

**Checkpoint:** `javac -d out $(find src/main -name '*.java')` can't succeed yet because `LinkService` and the storage classes don't exist. Keep going; the first real compile is at Step 5.

### Step 3: Add a registry factory that picks a class by name

**Goal:** turn `"hash"` into a `CodeGenerator` without a growing `if`/`else` chain, and let other code plug in new kinds.

`src/main/java/dev/ggorantala/shortener/codes/CodeGenerators.java`

```java
package dev.ggorantala.shortener.codes;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
 * Factory with a registry. Callers ask for a kind by name and never see the
 * concrete classes, which stay package-private.
 */
public final class CodeGenerators {

    private static final Map<String, Supplier<CodeGenerator>> REGISTRY = new ConcurrentHashMap<>();

    static {
        REGISTRY.put("sequential", SequentialCodeGenerator::new);
        REGISTRY.put("random", RandomCodeGenerator::new);
        REGISTRY.put("hash", HashCodeGenerator::new);
    }

    private CodeGenerators() { }

    public static CodeGenerator create(String kind) {
        Supplier<CodeGenerator> supplier = REGISTRY.get(kind);
        if (supplier == null) {
            throw new IllegalArgumentException(
                    "unknown code generator '" + kind + "', known: " + new java.util.TreeSet<>(REGISTRY.keySet()));
        }
        return supplier.get();
    }

    /** Lets other modules (or tests) plug in a kind without editing this class. */
    public static void register(String kind, Supplier<CodeGenerator> supplier) {
        if (REGISTRY.putIfAbsent(kind, supplier) != null) {
            throw new IllegalStateException("kind already registered: " + kind);
        }
    }
}

```

The map holds `Supplier`s, not instances. That matters: `SequentialCodeGenerator` keeps a counter, so each call to `create("sequential")` must return a fresh one, or two services would share ids. I check that in the tests.

Why a registry instead of a `switch`? A `switch` on a string is fine for three kinds, and in Step 4 I use one. The registry earns its keep when the list of kinds isn't yours to edit: `register("fixed", ...)` lets a test or a plugin add a kind from the outside.

### Step 4: Add an abstract factory for things that must match

**Goal:** make sure the link store and the click log always come from the same back end.

The first two factories each build *one* kind of object. An **abstract factory** builds a *family* of related objects. Here the family is two things: where links are stored and where clicks are counted. If links live in a file but clicks live in memory, you restart the app and every click count silently resets to zero. I'll show that bug on purpose in the troubleshooting section.

`src/main/java/dev/ggorantala/shortener/storage/LinkStore.java`

```java
package dev.ggorantala.shortener.storage;

import dev.ggorantala.shortener.ShortLink;
import java.util.Optional;

public interface LinkStore {
    boolean putIfAbsent(ShortLink link);
    Optional<ShortLink> find(String code);
    int size();
}

```

`src/main/java/dev/ggorantala/shortener/storage/ClickLog.java`

```java
package dev.ggorantala.shortener.storage;

public interface ClickLog {
    void record(String code);
    long clicks(String code);
}

```

`src/main/java/dev/ggorantala/shortener/storage/StorageFactory.java`

```java
package dev.ggorantala.shortener.storage;

import java.nio.file.Path;
import java.util.Map;

/**
 * Abstract factory: one object that produces a matching family of products.
 * A store and a click log from the same factory always agree on where data lives.
 */
public interface StorageFactory {
    LinkStore linkStore();
    ClickLog clickLog();

    /** Picks a family from configuration, e.g. storage=memory or storage=file. */
    static StorageFactory fromConfig(Map<String, String> config) {
        String kind = config.getOrDefault("storage", "memory");
        return switch (kind) {
            case "memory" -> new InMemoryStorageFactory();
            case "file" -> new FileStorageFactory(Path.of(
                    config.getOrDefault("storage.dir", "data")));
            default -> throw new IllegalArgumentException(
                    "storage must be memory or file, got: " + kind);
        };
    }
}

```

The `fromConfig` method is a static factory on the interface itself, and it uses a switch expression (see [switch expressions and pattern matching](https://ggorantala.dev/how-to-use-switch-expressions-pattern-matching-java/?ref=ggorantala.dev)) so there's no fall-through to get wrong. Now the two families.

`src/main/java/dev/ggorantala/shortener/storage/InMemoryStorageFactory.java`

```java
package dev.ggorantala.shortener.storage;

import dev.ggorantala.shortener.ShortLink;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

public final class InMemoryStorageFactory implements StorageFactory {

    // One shared instance per factory, so linkStore() called twice means the same store.
    private final LinkStore store = new MemoryLinkStore();
    private final ClickLog log = new MemoryClickLog();

    @Override public LinkStore linkStore() { return store; }
    @Override public ClickLog clickLog() { return log; }

    private static final class MemoryLinkStore implements LinkStore {
        private final Map<String, ShortLink> links = new ConcurrentHashMap<>();
        @Override public boolean putIfAbsent(ShortLink link) {
            return links.putIfAbsent(link.code(), link) == null;
        }
        @Override public Optional<ShortLink> find(String code) {
            return Optional.ofNullable(links.get(code));
        }
        @Override public int size() { return links.size(); }
    }

    private static final class MemoryClickLog implements ClickLog {
        private final Map<String, LongAdder> counts = new ConcurrentHashMap<>();
        @Override public void record(String code) {
            counts.computeIfAbsent(code, c -> new LongAdder()).increment();
        }
        @Override public long clicks(String code) {
            LongAdder a = counts.get(code);
            return a == null ? 0 : a.sum();
        }
    }
}

```

`src/main/java/dev/ggorantala/shortener/storage/FileStorageFactory.java`

```java
package dev.ggorantala.shortener.storage;

import dev.ggorantala.shortener.ShortLink;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Optional;

/** Both products append to files in one directory, so they can't drift apart. */
public final class FileStorageFactory implements StorageFactory {

    private final LinkStore store;
    private final ClickLog log;

    public FileStorageFactory(Path dir) {
        try {
            Files.createDirectories(dir);
        } catch (IOException e) {
            throw new UncheckedIOException("cannot create " + dir, e);
        }
        this.store = new FileLinkStore(dir.resolve("links.tsv"));
        this.log = new FileClickLog(dir.resolve("clicks.log"));
    }

    @Override public LinkStore linkStore() { return store; }
    @Override public ClickLog clickLog() { return log; }

    private static List<String> lines(Path file) {
        try {
            return Files.exists(file) ? Files.readAllLines(file) : List.of();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static void append(Path file, String line) {
        try {
            Files.writeString(file, line + System.lineSeparator(),
                    StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static final class FileLinkStore implements LinkStore {
        private final Path file;
        FileLinkStore(Path file) { this.file = file; }

        @Override public synchronized boolean putIfAbsent(ShortLink link) {
            if (find(link.code()).isPresent()) return false;
            append(file, link.toLine());
            return true;
        }
        @Override public synchronized Optional<ShortLink> find(String code) {
            return lines(file).stream().map(ShortLink::parse)
                    .filter(l -> l.code().equals(code)).findFirst();
        }
        @Override public synchronized int size() { return lines(file).size(); }
    }

    private static final class FileClickLog implements ClickLog {
        private final Path file;
        FileClickLog(Path file) { this.file = file; }

        @Override public synchronized void record(String code) { append(file, code); }
        @Override public synchronized long clicks(String code) {
            return lines(file).stream().filter(code::equals).count();
        }
    }
}

```

Notice that the factory creates each product once, in its constructor, and `linkStore()` hands back the same instance every time. If it built a new store on each call, `linkService.shorten()` would write into one map and `resolve()` would read from another. The file version is deliberately naive: it re-reads the whole file on every lookup. That's fine for a tutorial and wrong for production; see "What to change before real traffic".

(The in-memory pieces use [ConcurrentHashMap](https://ggorantala.dev/how-to-use-concurrenthashmap-java/?ref=ggorantala.dev) and `LongAdder`, which I covered in the concurrency tutorials, so the service is safe to call from several threads.)

### Step 5: Write the service that doesn't know any of this

**Goal:** prove the factories pay off. `LinkService` should compile against interfaces only.

`src/main/java/dev/ggorantala/shortener/LinkService.java`

```java
package dev.ggorantala.shortener;

import dev.ggorantala.shortener.codes.CodeGenerator;
import dev.ggorantala.shortener.storage.ClickLog;
import dev.ggorantala.shortener.storage.LinkStore;
import dev.ggorantala.shortener.storage.StorageFactory;
import java.net.URI;
import java.time.Clock;
import java.util.Optional;

/** Depends on interfaces only. It has no idea whether data lives in memory or on disk. */
public final class LinkService {
    private final CodeGenerator generator;
    private final LinkStore store;
    private final ClickLog clicks;
    private final Clock clock;

    public LinkService(CodeGenerator generator, StorageFactory storage, Clock clock) {
        this.generator = generator;
        this.store = storage.linkStore();
        this.clicks = storage.clickLog();
        this.clock = clock;
    }

    public ShortLink shorten(String target) {
        URI.create(target); // fail early on garbage
        for (int attempt = 0; attempt < 5; attempt++) {
            ShortLink link = ShortLink.of(generator.next(target), target, clock.instant());
            if (store.putIfAbsent(link)) return link;
            // Same target again with a hash generator: return the existing link.
            Optional<ShortLink> existing = store.find(link.code());
            if (existing.isPresent() && existing.get().target().equals(link.target())) {
                return existing.get();
            }
        }
        throw new IllegalStateException("could not find a free code for " + target);
    }

    public Optional<URI> resolve(String code) {
        Optional<ShortLink> link = store.find(code);
        link.ifPresent(l -> clicks.record(code));
        return link.map(ShortLink::target);
    }

    public long clicks(String code) { return clicks.clicks(code); }
}

```

`src/main/java/dev/ggorantala/shortener/Main.java`

```java
package dev.ggorantala.shortener;

import dev.ggorantala.shortener.codes.CodeGenerators;
import dev.ggorantala.shortener.storage.StorageFactory;
import java.time.Clock;
import java.util.Map;

public final class Main {
    public static void main(String[] args) {
        String gen = args.length > 0 ? args[0] : "sequential";
        String storage = args.length > 1 ? args[1] : "memory";
        Map<String, String> config = Map.of("storage", storage, "storage.dir", "build/data");

        LinkService service = new LinkService(
                CodeGenerators.create(gen), StorageFactory.fromConfig(config), Clock.systemUTC());

        var a = service.shorten("https://ggorantala.dev/how-to-use-java-records/");
        var b = service.shorten("https://ggorantala.dev/how-to-use-optional-java/");
        System.out.println("generator=" + gen + " storage=" + storage);
        System.out.println(a.code() + " -> " + a.target());
        System.out.println(b.code() + " -> " + b.target());

        service.resolve(a.code());
        service.resolve(a.code());
        service.resolve(b.code());
        System.out.println("clicks " + a.code() + " = " + service.clicks(a.code()));
        System.out.println("clicks " + b.code() + " = " + service.clicks(b.code()));
        System.out.println("unknown code -> " + service.resolve("nope"));
    }
}

```

Look at the imports in `LinkService`: `CodeGenerator`, `ClickLog`, `LinkStore`, `StorageFactory`. Not one concrete class. That's the payoff. If you later add `RedisStorageFactory`, this file doesn't change.

**Checkpoint:** compile with warnings on.

```bash
javac -Xlint:all -d out $(find src/main -name '*.java')

```

A clean compile prints nothing from javac. (My shell also printed a `Picked up JAVA_TOOL_OPTIONS` line, which comes from the environment, not from your code.) It compiled first time for me, which made me suspicious enough to go hunting for ways to break it. Those are in the troubleshooting section.

## Swap generators and storage from the command line

Each run below is the real output from my machine.

```bash
java -cp out dev.ggorantala.shortener.Main sequential memory
java -cp out dev.ggorantala.shortener.Main hash file
java -cp out dev.ggorantala.shortener.Main random memory

```

The first two are at the top of this article. The third:

```log
generator=random storage=memory
oVqvLzM -> https://ggorantala.dev/how-to-use-java-records/
cc84tpz -> https://ggorantala.dev/how-to-use-optional-java/
clicks oVqvLzM = 2
clicks cc84tpz = 1
unknown code -> Optional.empty

```

Your random codes will differ. Now look at what the file family left on disk:

```bash
ls build/data
cat build/data/*

```

```log
clicks.log
links.tsv
1CMqNAHRO
1CMqNAHRO
1DQhMUpnP
1CMqNAHRO	https://ggorantala.dev/how-to-use-java-records/	1793068046980
1DQhMUpnP	https://ggorantala.dev/how-to-use-optional-java/	1793068046996

```

`cat` printed `clicks.log` first (three clicks), then `links.tsv` (two links with epoch-millisecond timestamps). Run the `hash file` command again and you'll see `shorten` return the *same* two codes without adding lines to `links.tsv`, because the hash generator maps the same URL to the same code and the service recognises the existing link. Your timestamps will differ from mine.

## Check it with 26 assertions

`src/test/java/dev/ggorantala/shortener/Checks.java`

```java
package dev.ggorantala.shortener;

import dev.ggorantala.shortener.codes.*;
import dev.ggorantala.shortener.storage.*;
import java.nio.file.*;
import java.time.*;
import java.util.*;

public final class Checks {
    static int passed, failed;

    static void check(String name, boolean ok) {
        if (ok) passed++; else failed++;
        System.out.println((ok ? "PASS " : "FAIL ") + name);
    }

    static boolean throwsIae(Runnable r) {
        try { r.run(); return false; } catch (IllegalArgumentException e) { return true; }
    }

    public static void main(String[] args) throws Exception {
        Instant t = Instant.parse("2026-10-27T00:00:00Z");
        Clock clock = Clock.fixed(t, ZoneOffset.UTC);

        check("ShortLink.of builds a valid link", ShortLink.of("abc", "https://a.dev/x", t).code().equals("abc"));
        check("bad code rejected", throwsIae(() -> ShortLink.of("a b", "https://a.dev", t)));
        check("empty code rejected", throwsIae(() -> ShortLink.of("", "https://a.dev", t)));
        check("ftp scheme rejected", throwsIae(() -> ShortLink.of("abc", "ftp://a.dev", t)));
        check("parse round-trips toLine", ShortLink.parse(ShortLink.of("abc", "https://a.dev/x", t).toLine())
                .equals(ShortLink.of("abc", "https://a.dev/x", t)));
        check("parse rejects wrong field count", throwsIae(() -> ShortLink.parse("abc\thttps://a.dev")));

        check("sequential is predictable",
                CodeGenerators.create("sequential").next("x").equals("G9"));
        CodeGenerator seq = CodeGenerators.create("sequential");
        seq.next("x");
        check("sequential increments", seq.next("x").equals("GA"));
        check("each create() gives a fresh generator",
                CodeGenerators.create("sequential") != CodeGenerators.create("sequential"));
        check("hash is deterministic",
                CodeGenerators.create("hash").next("u").equals(CodeGenerators.create("hash").next("u")));
        check("hash differs per target",
                !CodeGenerators.create("hash").next("u1").equals(CodeGenerators.create("hash").next("u2")));
        check("random yields valid codes", ShortLink.of(CodeGenerators.create("random").next("u"),
                "https://a.dev", t) != null);
        check("unknown kind rejected", throwsIae(() -> CodeGenerators.create("nope")));
        CodeGenerators.register("fixed", () -> target -> "ZZ");
        check("register plugs in a new kind", CodeGenerators.create("fixed").next("u").equals("ZZ"));
        boolean dup = false;
        try { CodeGenerators.register("fixed", () -> target -> "YY"); } catch (IllegalStateException e) { dup = true; }
        check("duplicate registration rejected", dup);

        StorageFactory mem = StorageFactory.fromConfig(Map.of());
        check("default storage is memory", mem instanceof InMemoryStorageFactory);
        check("linkStore() returns the same instance", mem.linkStore() == mem.linkStore());
        check("unknown storage rejected", throwsIae(() -> StorageFactory.fromConfig(Map.of("storage", "s3"))));

        LinkService svc = new LinkService(CodeGenerators.create("sequential"), mem, clock);
        ShortLink a = svc.shorten("https://a.dev/1");
        check("shorten stores and resolve finds", svc.resolve(a.code()).get().toString().equals("https://a.dev/1"));
        svc.resolve(a.code());
        check("clicks counted", svc.clicks(a.code()) == 2);
        check("unknown code resolves empty", svc.resolve("nope").isEmpty());
        check("unknown code is not counted", svc.clicks("nope") == 0);

        LinkService hashSvc = new LinkService(CodeGenerators.create("hash"), StorageFactory.fromConfig(Map.of()), clock);
        check("same target twice returns same link", hashSvc.shorten("https://a.dev/1").equals(hashSvc.shorten("https://a.dev/1")));

        Path dir = Files.createTempDirectory("shortener");
        StorageFactory file = StorageFactory.fromConfig(Map.of("storage", "file", "storage.dir", dir.toString()));
        LinkService f1 = new LinkService(CodeGenerators.create("hash"), file, clock);
        ShortLink fl = f1.shorten("https://a.dev/persist");
        f1.resolve(fl.code());
        LinkService f2 = new LinkService(CodeGenerators.create("hash"),
                new FileStorageFactory(dir), clock);
        check("file family survives a restart (link)", f2.resolve(fl.code()).isPresent());
        check("file family survives a restart (clicks)", f2.clicks(fl.code()) == 2);
        check("file store has one line", Files.readAllLines(dir.resolve("links.tsv")).size() == 1);

        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }
}

```

Compile and run:

```bash
javac -Xlint:all -d out -cp out src/test/java/dev/ggorantala/shortener/Checks.java
java -cp out dev.ggorantala.shortener.Checks

```

```log
PASS ShortLink.of builds a valid link
PASS bad code rejected
PASS empty code rejected
PASS ftp scheme rejected
PASS parse round-trips toLine
PASS parse rejects wrong field count
PASS sequential is predictable
PASS sequential increments
PASS each create() gives a fresh generator
PASS hash is deterministic
PASS hash differs per target
PASS random yields valid codes
PASS unknown kind rejected
PASS register plugs in a new kind
PASS duplicate registration rejected
PASS default storage is memory
PASS linkStore() returns the same instance
PASS unknown storage rejected
PASS shorten stores and resolve finds
PASS clicks counted
PASS unknown code resolves empty
PASS unknown code is not counted
PASS same target twice returns same link
PASS file family survives a restart (link)
PASS file family survives a restart (clicks)
PASS file store has one line
26 passed, 0 failed

```

To move these to JUnit 5, add `org.junit.jupiter:junit-jupiter` as a test dependency (check the current version on Maven Central; I couldn't), turn each `check(...)` into an `@Test` with `assertThat(...)` from AssertJ, and turn `throwsIae` into `assertThrows(IllegalArgumentException.class, ...)`. The unit-testing tutorial in this series ([how to write unit tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev), coming) covers the setup.

The check I care about most is `linkStore() returns the same instance`. It's the one that catches the "factory builds a new product on every call" mistake.

## Why callers never see the concrete classes

Three ideas are doing the work, and they get mixed up a lot.

A **static factory method** (`ShortLink.of`) is a named constructor. It doesn't hide a class; it gives construction a name and a place to put checks. Joshua Bloch's *Effective Java* lists several advantages; the one I find real in daily work is the name.

A **factory with a registry** (`CodeGenerators`) hides the implementing classes. Because `SequentialCodeGenerator` and friends are package-private, the compiler enforces that nobody outside the package can depend on them. You can rename, split or replace them without touching a caller.

An **abstract factory** (`StorageFactory`) is for *families*. The question to ask is: "do these objects only make sense together?" If a `LinkStore` and a `ClickLog` can be mixed freely, you don't need one; two separate factories would do. They can't, so one factory builds both.

What I'd skip: a `FactoryOfFactories`, or a factory for a class that has one implementation and no validation. If the only caller is `new Foo()` and `Foo` will never have a sibling, the factory is noise. I'd wait for the second implementation before extracting an interface, then add the factory. The [strategy pattern tutorial](https://ggorantala.dev/how-to-implement-strategy-pattern-java/?ref=ggorantala.dev) is the cousin of this one: strategy swaps behaviour at runtime, a factory decides what to build in the first place. They often travel together, as `CodeGenerator` does here.

## Mistakes I made on purpose

I tried to break every promise the article makes. These are the failures, with the exact messages.

**Calling `new` on a hidden implementation.** From a class outside the package:

```java
CodeGenerator g = new SequentialCodeGenerator();

```

```log
T1.java:2: error: cannot find symbol
public class T1 { public static void main(String[] a) { CodeGenerator g = new SequentialCodeGenerator(); } }
                                                                              ^
  symbol:   class SequentialCodeGenerator
  location: class T1
1 error

```

Cause: the class isn't `public`, so it doesn't exist as far as other packages are concerned. Fix: call `CodeGenerators.create("sequential")`. This error is the pattern working, not failing.

**A typo in the kind name.** I wrote `"sequencial"`:

```log
Exception in thread "main" java.lang.IllegalArgumentException: unknown code generator 'sequencial', known: [hash, random, sequential]
	at dev.ggorantala.shortener.codes.CodeGenerators.create(CodeGenerators.java:27)

```

Cause: strings aren't checked by the compiler. Fix: the message lists the valid kinds, which is exactly why I put them there. If kinds are fixed at compile time, use an `enum` instead and the typo becomes a compile error.

**A storage name in the wrong case.** `Map.of("storage","File")`:

```log
Exception in thread "main" java.lang.IllegalArgumentException: storage must be memory or file, got: File
	at dev.ggorantala.shortener.storage.StorageFactory.fromConfig(StorageFactory.java:21)

```

Fix: lower-case, or call `toLowerCase(Locale.ROOT)` before the switch if you want to be forgiving. I left it strict so config mistakes fail at startup rather than quietly picking a default.

**A link nobody should be able to build.** `ShortLink.of("ok", "javascript:alert(1)", Instant.now())`:

```log
Exception in thread "main" java.lang.IllegalArgumentException: target must be an http(s) URI: javascript:alert(1)
	at dev.ggorantala.shortener.ShortLink.<init>(ShortLink.java:16)
	at dev.ggorantala.shortener.ShortLink.of(ShortLink.java:25)

```

The stack shows `<init>` being called from `of`, which is the compact constructor doing its job. A shortener that redirects to `javascript:` URLs is an open door, so this check is not decoration.

**Mixing families.** This is the bug the abstract factory exists to prevent. I wrote a throwaway `StorageFactory` that takes the link store from the in-memory family and the click log from the file family, then ran the same service twice, as if the app had restarted:

```log
run 1: code=G9 clicks=1
run 2: code=G9 clicks=2

```

Look closely. Run 2 shows `clicks=2` for a link that, from its own point of view, was created seconds ago and clicked once. The file kept the first run's click; memory forgot the link. No exception, no warning, just numbers that are quietly wrong. That's the failure mode to worry about with mixed families: it doesn't crash, it lies. It's the most useful thing I learned writing this article.

## What to change before real traffic

- **Don't ship the file back end.** It re-reads the whole file for every lookup and takes a `synchronized` lock per store. It exists to show a second family. A real second family would be JDBC or Redis, built by a `JdbcStorageFactory` with a connection pool inside it.
- **Handle `random` collisions deliberately.** `LinkService.shorten` retries up to five times on a clash. That's enough for a demo and arbitrary for production; pick the retry limit from your id space and traffic.
- **Don't use `hash` where URLs are private.** It's deterministic, so anyone who guesses a URL can compute its code.
- **Let a framework build the objects when you have one.** In Spring Boot, the container is the factory and profiles (see the upcoming [Spring Boot profiles tutorial](https://ggorantala.dev/how-to-use-spring-boot-profiles-configuration/?ref=ggorantala.dev)) pick the family. Hand-written abstract factories are mostly for code that runs outside a container.
- **Version note.** Nothing here needs Java 21 specifically. Records and switch expressions need 16 or 14+; `--release 17` compiled all main sources cleanly. On Java 11 you'd replace the record with a final class.

If you read the earlier tutorials, [the builder pattern](https://ggorantala.dev/how-to-implement-builder-pattern-java/?ref=ggorantala.dev) is the right tool when an object has many optional fields, [records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev) and [immutable classes](https://ggorantala.dev/how-to-create-immutable-class-java/?ref=ggorantala.dev) cover why `ShortLink` is shaped the way it is, and [sealed classes](https://ggorantala.dev/how-to-use-sealed-classes-interfaces-java/?ref=ggorantala.dev) are the next step if you want the compiler to know every possible generator kind.

## Rules to keep

- Start with a static factory method (`of`, `parse`, `from`) when you need a name or validation.
- Hide implementations behind an interface and package-private classes; let a factory be the only place that says `new`.
- Use a registry (`Map<String, Supplier<T>>`) when the list of kinds isn't yours to edit; use an `enum` or `switch` when it is.
- Use an abstract factory only for products that must match, and make it create each product once.
- Test that each factory call returns what you think: fresh instance or shared one.
- Fail loudly on unknown names, and list the valid ones in the message.

## Questions people ask about the factory pattern in Java

**What's the difference between the factory pattern and the abstract factory pattern?**  
A factory builds one kind of object and decides which class to use. An abstract factory builds a group of related objects that belong together, like the store and click log here. If your products don't depend on each other, you don't need the abstract version.

**Is a static factory method the same as the factory pattern?**  
Not quite. A static factory method is a named, static way to create an instance, and it's a close relative that doesn't have to hide any class. The classic factory pattern hides which implementation you get back. In practice both are useful, and Step 1 and Step 3 show each.

**When should I use an enum instead of a registry?**  
Use an enum when every kind is known at compile time and you want typos to be compile errors. Use a registry when other modules need to add kinds. I started with the registry here because I wanted `register` to exist for the tests.

**Do I need a factory if I'm using Spring?**  
Usually not. Spring's container, `@Bean` methods and profiles do the job for most cases. Hand-written factories matter when you create objects at runtime based on data, or when you're outside a container.

## References

- Java SE 21 API docs: [java.util.function.Supplier](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/function/Supplier.html?ref=ggorantala.dev), [ConcurrentHashMap](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ConcurrentHashMap.html?ref=ggorantala.dev), [MessageDigest](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/security/MessageDigest.html?ref=ggorantala.dev)
- [JEP 395: Records](https://openjdk.org/jeps/395?ref=ggorantala.dev) and [JEP 361: Switch Expressions](https://openjdk.org/jeps/361?ref=ggorantala.dev)