The Abstract Factory pattern provides an interface for creating a family of related products without exposing their concrete classes to the client. Each creation method returns a different product type. A concrete factory supplies the implementations intended to work together.
The client receives one factory and requests its products through that interface. Changing the concrete factory changes the product family, while the client continues using the same product operations. A family therefore means compatible implementations of several product types, not just several alternatives for one type.
1. How the Abstract Factory Pattern Works
An abstract factory declares the products that a client needs. Each concrete factory implements those creation methods for one family. The client uses the returned product interfaces instead of constructing the concrete products itself.
For example, if a client needs both a writer and a reader, one factory can supply the pair for a particular data format. Selecting the writer and reader separately would allow the application to select incompatible implementations. Obtaining both from one correctly implemented factory keeps that decision in one place.
The participants divide creation from usage:
| Participant | Responsibility |
|---|---|
| Abstract factory | Declares a creation method for each product type. |
| Concrete factory | Supplies the related implementations for one family. |
| Abstract products | Define the operations the client needs from each product type. |
| Concrete products | Implement those operations for a particular family. |
| Client | Obtains products from a factory and uses their interfaces. |
The factory implementation provides family consistency; the pattern does not make incompatible products impossible to construct. A faulty factory can still return a mismatched pair. The common Java interfaces describe operations, but they do not automatically express which implementations belong together.
2. Abstract Factory Pattern in Action
A notes application lets users back up their notes and restore them later. It supports plain-text backups and GZIP-compressed backups. In either format, the application needs a writer to turn a note into backup bytes and a reader to recover the text from those bytes.
Both formats use UTF-8, an encoding that represents text as bytes. The GZIP format compresses those bytes before returning them and decompresses them before decoding the text. Its reader must therefore match its writer. Passing plain-text bytes to a GZIP reader does not produce a valid restore operation.
Our example keeps the backup bytes in memory so the creation and restore behavior remain visible. Saving those bytes to a file is a separate storage operation.
| Role | Text family | GZIP family |
|---|---|---|
| BackupWriter | TextBackupWriter encodes the note as UTF-8. | GzipBackupWriter compresses the UTF-8 bytes. |
| BackupReader | TextBackupReader decodes UTF-8 bytes. | GzipBackupReader decompresses and then decodes them. |
| BackupFactory | TextBackupFactory supplies the text pair. | GzipBackupFactory supplies the compressed pair. |
BackupService is the client. It receives a factory, requests both products, and delegates backup and restore operations to them. The application selects the GZIP family with the following calls. They run inside a method that declares or handles IOException:
BackupService service = new BackupService(new GzipBackupFactory());
byte[] data = service.backup("Meeting at 10:00 - café");
System.out.println(service.restore(data));
The restored note is:
Meeting at 10:00 - café
The service does not contain a format-selection condition. Its constructor receives the selected family through BackupFactory. Application setup still chooses that factory; Abstract Factory does not make the choice automatically.
3. Implementing the Product Families
The example uses JDK 17 or later and only standard Java APIs. Each public type below belongs in its own file. Package declarations are omitted from the excerpts and included in the complete source.
3.1. Defining the Writer and Reader Contracts
The two product interfaces represent complementary operations. BackupWriter.write() returns backup bytes, while BackupReader.read() returns the restored text:
import java.io.IOException;
public interface BackupWriter {
byte[] write(String text) throws IOException;
}
import java.io.IOException;
public interface BackupReader {
String read(byte[] data) throws IOException;
}
Both operations declare IOException because reading or writing a compressed stream can fail. The client can handle that checked exception or declare it on its own method. The text implementations do not need to declare it because their operations do not throw it.
3.2. Creating the Text Products
The text writer encodes the note with an explicit character set. The reader uses the same character set to reverse that operation:
import java.nio.charset.StandardCharsets;
public final class TextBackupWriter implements BackupWriter {
@Override
public byte[] write(String text) {
return text.getBytes(StandardCharsets.UTF_8);
}
}
import java.nio.charset.StandardCharsets;
public final class TextBackupReader implements BackupReader {
@Override
public String read(byte[] data) {
return new String(data, StandardCharsets.UTF_8);
}
}
Using StandardCharsets.UTF_8 on both sides makes the encoding independent of the machine’s default charset. The accented character in our note exercises this behavior; its encoded representation must be decoded with the matching character set.
These products do not add compression or a format header. The caller must already know that the supplied bytes are a text backup. The service will check for missing arguments before invoking either product.
3.3. Creating the GZIP Products
The GZIP writer first encodes the note as UTF-8, then writes those bytes through a compression stream. A ByteArrayOutputStream collects the compressed result in memory:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPOutputStream;
public final class GzipBackupWriter implements BackupWriter {
@Override
public byte[] write(String text) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(buffer)) {
gzip.write(text.getBytes(StandardCharsets.UTF_8));
}
return buffer.toByteArray();
}
}
The try-with-resources statement closes GZIPOutputStream before the method reads the completed buffer. The inherited close() operation finishes its compressed output. Returning the buffer while the compression stream is still open could leave the backup incomplete. Closing the underlying ByteArrayOutputStream does not prevent calling toByteArray() afterward.
The matching reader reverses those operations. It reads the compressed bytes through GZIPInputStream, obtains the decompressed bytes, and decodes them as UTF-8:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;
public final class GzipBackupReader implements BackupReader {
@Override
public String read(byte[] data) throws IOException {
try (GZIPInputStream gzip = new GZIPInputStream(
new ByteArrayInputStream(data))) {
return new String(gzip.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
GZIPInputStream checks the GZIP format while reading. A backup from the text writer is not valid input for this reader. We will exercise that mismatch after the successful restore scenarios.
This implementation reads the complete decompressed note into memory. It is suitable for the small, trusted inputs in the example. A backup service for large files would stream data instead; handling untrusted compressed input also requires an explicit limit on decompressed size.
4. Creating and Using a Complete Family
The factory interface groups the two creation operations. The client does not need to know whether either returned product performs compression:
public interface BackupFactory {
BackupWriter createWriter();
BackupReader createReader();
}
4.1. Implementing the Concrete Factories
TextBackupFactory constructs both text products. Keeping both methods in the same factory makes its family choice explicit:
public final class TextBackupFactory implements BackupFactory {
@Override
public BackupWriter createWriter() {
return new TextBackupWriter();
}
@Override
public BackupReader createReader() {
return new TextBackupReader();
}
}
GzipBackupFactory implements the same contract but returns both GZIP products:
public final class GzipBackupFactory implements BackupFactory {
@Override
public BackupWriter createWriter() {
return new GzipBackupWriter();
}
@Override
public BackupReader createReader() {
return new GzipBackupReader();
}
}
Each method returns a product interface even though its constructor creates a concrete class. The implementations use new on each request, but the pattern does not require that lifecycle policy. A factory could reuse a suitable stateless product if that behavior fits its contract.
4.2. Keeping Format Selection Outside the Client
BackupService obtains its writer and reader from the supplied factory during construction. It stores those products and reuses them for later calls:
import java.io.IOException;
import java.util.Objects;
public final class BackupService {
private final BackupWriter writer;
private final BackupReader reader;
public BackupService(BackupFactory factory) {
Objects.requireNonNull(factory, "factory is required");
writer = factory.createWriter();
reader = factory.createReader();
}
public byte[] backup(String text) throws IOException {
return writer.write(Objects.requireNonNull(text, "text is required"));
}
public String restore(byte[] data) throws IOException {
return reader.read(Objects.requireNonNull(data, "data is required"));
}
}
The constructor rejects a missing factory with Objects.requireNonNull(). The public operations also reject missing text or bytes before delegating. Empty text remains valid; it is different from a missing value.
When backup() runs, the selected writer creates a byte array. When restore() runs, the selected reader consumes that array and returns text. The service has no concrete product names and no conditional branch for GZIP.
Changing the factory changes both products used by a new service instance. It does not replace products already stored in an existing service. To use another family here, construct another service with the corresponding factory.
5. Running the Example
The complete AbstractFactoryDemo creates one service for each format. Compile and run it from the repository root:
javac -encoding UTF-8 -d out src/main/java/com/howtodoinjava/patterns/creational/abstractfactory/*.java
java -cp out com.howtodoinjava.patterns.creational.abstractfactory.AbstractFactoryDemo
The UTF-8 compiler option preserves the accented character in the source file. The following scenarios show how the same client behaves with each family.
5.1. Restoring With the Matching Family
Both services receive the same note. Each restores the bytes produced by its own writer:
String note = "Meeting at 10:00 - café";
BackupService text = new BackupService(new TextBackupFactory());
BackupService gzip = new BackupService(new GzipBackupFactory());
System.out.println("Text restored: " + text.restore(text.backup(note)));
System.out.println("GZIP restored: " + gzip.restore(gzip.backup(note)));
System.out.println("Empty note restored: " + gzip.restore(gzip.backup("")).isEmpty());
The output confirms matching text and GZIP round trips, including an empty note:
Text restored: Meeting at 10:00 - café
GZIP restored: Meeting at 10:00 - café
Empty note restored: true
The two backup byte representations differ, but both readers recover the original text. The factory creates the writer and reader instances; their methods create and consume the backup data. Compression is not encryption, and a short note is not guaranteed to become smaller after adding the GZIP format overhead.
5.2. Attempting to Restore a Different Format
Now pass the text writer’s bytes to the GZIP service:
try {
gzip.restore(text.backup(note));
} catch (IOException exception) {
System.out.println("Wrong reader: incompatible backup rejected");
}
The result is:
Wrong reader: incompatible backup rejected
The call compiles because both services exchange byte[]. It fails when the GZIP reader encounters bytes that do not have the required format. This is a reader check, not an automatic guarantee supplied by Abstract Factory. The reverse mismatch is not guaranteed to fail: the text reader can decode compressed bytes into meaningless text instead.
For persisted backups, store a format identifier with the data or in trusted metadata. Restore code can then select the corresponding factory. The example deliberately requires an explicit choice and performs no format detection.
6. When to Use Abstract Factory
Abstract Factory fits when an application needs several related product types and must select their implementations together. It also lets tests supply a different factory without changing the client. That benefit comes with additional interfaces and implementations.
The two extension cases have different costs:
| Change | Required work |
|---|---|
| Add another backup format | Implement its writer, reader and concrete factory; update application selection. |
| Add a new product type, such as a backup inspector | Extend the factory contract and implement that operation in every concrete factory. |
A Factory Method lets a subclass supply a product to an inherited workflow. Abstract Factory instead supplies a group of products through a factory instance. Its creation methods can themselves use Factory Method, but these patterns solve different design problems.
If writing and reading always belong in one small component, a single codec interface that combines encoding and decoding may be simpler than separate products and a factory. Separate products become useful when different clients need them independently or their implementations have separate dependencies. Dependency injection can also supply the factory to the service; it does not replace the need to choose a compatible family.
7. Frequently Asked Questions
The name of the pattern can suggest requirements that Java does not impose. These distinctions help keep the implementation focused on product families.
7.1. Must the Factory Be an abstract Class?
No. The abstract factory is a design role. An interface, as used by BackupFactory, can express that contract. An abstract class is useful when factory implementations also need shared implementation code or state.
7.2. Does Abstract Factory Require a Factory of Factories?
No. Application setup can construct a concrete factory directly and pass it to the client. A separate selector may help when configuration determines the family, but adding another factory layer is not a requirement of the pattern. The essential relationship is one factory supplying related product types.
7.3. Does a Factory Have to Be a Singleton?
No. Factory lifetime and product-family selection are separate concerns. The factories here contain no state and can be passed to several clients, but they do not enforce a single instance. Use a Singleton only when the application has a separate reason to restrict instance creation.
8. Conclusion
Use Abstract Factory when several product choices need to remain consistent. Keep the family selection in the concrete factory and let the client use the product interfaces. Define format and failure behavior explicitly: a shared interface does not prove that data or products from different families are interchangeable.
Happy Learning!
The complete example is available in the GitHub source folder.
As per Abstract Factory pattern , It provides an interface for creating families of related or dependent objects without specifying their concrete class. but here the factory is returning only Car type. I think this a Factory method pattern with multiple concrete factory.
Reference: https://en.wikipedia.org/wiki/Abstract_factory_pattern
Could anyone please explain which one is correct implementation and why?
Hi Lokesh,
I just want to understand whether location should be part of “Car”?
Car has nothing to do with location then why did you put location as part of Car?
//Bhupesh
Given that each car instance in this application is going to be strictly location specific, how would you determine the location attribute of a random car instance in runtime of application if it’s not tied to car instance itself.
Shouldn’t class CarFactory be abstract instead of setting the constructor to private?
CarFactory method needs to be modified every time for switch case , when a location gets added. How could we make it extensible without modification.
Hi Supun, In my view interfaces should be used for adding the behavior to classes only. Here, we are creating instances of Car only in different ways. I think that abstract class make more sense here.
BUT, there is no such restriction from design pattern side, you are free to implement using interfaces as well if you find it more easy. It’s matter of choice only.
Nice article about abstract factory design pattern.
Thanks Lokesh.Very nice explanation in simple words.
Hi Lokesh, Thank you for such nice and simple explanation. I have one question, for example I have two different factories one for fruit, one for Vegetable, question is how can I use abstract factory pattern here.
Talking about myself, I do not see any value addition in making abstract factory for your particular case. Fruits and Vegetables are two different entities and there are many major differences between them. So making two separate factories; one for each, makes more sense to me.
Also, factory (or abstract factory) pattern should be applied on objects which you can build in different ways; in other words; there can be different representations of a object which belong a common parent.
Your question is very good but to me, it’s really doesn’t fit into context.
how to get Location from a configuration file in CarFactory
Take help of this article:
It is surprised to me to see no abstract classes except CAR in your example ..
Abstract factory does not mandate abstract class usage. It’s pattern. Implementation can be of your choice.
Thanks for the article.Its explained well using the example of car.Now I need a real time scenario where this pattern is used(like factory is used in SessionFactory in hibernate)
Refer here [best link]: https://stackoverflow.com/questions/1673841/examples-of-gof-design-patterns-in-javas-core-libraries