Abstract Factory Pattern #

When an application has to run across several different environments — say AWS in production, GCP in staging, and mocks in unit tests — the problem is not just one object that differs, but the entire family of objects that must be swapped at once: storage, message queue, secret manager, monitoring, all of it has to come from the same provider and work consistently with each other. Swapping them one by one by hand in many places is not just tedious — it invites inconsistency: you could accidentally use AWS S3 alongside GCP Pub/Sub. The Abstract Factory Pattern solves this by providing a single point to swap the whole family of objects at once, without touching the code that uses them.

What Is the Abstract Factory Pattern? #

Abstract Factory is a creational design pattern that provides an interface for creating a set of related objects, without specifying their concrete classes. Instead of creating each object individually, the client asks one factory to create all the objects it needs — and that factory guarantees every object comes from the same “family” and is compatible with the rest.

The most fundamental difference between Abstract Factory and Factory Method: Factory Method handles one product, while Abstract Factory handles a product family. A single NotificationFactory can create a Sender, a Formatter, and an AuditLogger — all three come from the Email “family” or the SMS “family”, and are guaranteed to be compatible.

Three properties define Abstract Factory:

  • Objects are created as a family — one factory, many related products
  • Consistency is guaranteed — you cannot accidentally mix products from different families
  • The client knows no concrete classes — it only interacts with product interfaces and the factory interface
flowchart TD
    subgraph Client
        C[NotificationService]
    end

    subgraph Abstract Layer
        AF[NotificationFactory\\ninterface]
        AS[Sender\\ninterface]
        AFmt[Formatter\\ninterface]
        AL[AuditLogger\\ninterface]
    end

    subgraph Email Family
        EF[EmailFactory]
        ES[EmailSender]
        EFmt[EmailFormatter]
        EL[EmailAuditLogger]
    end

    subgraph SMS Family
        SF[SMSFactory]
        SS[SMSSender]
        SFmt[SMSFormatter]
        SL[SMSAuditLogger]
    end

    C -->|only knows| AF
    AF --> EF
    AF --> SF
    EF -->|creates| ES
    EF -->|creates| EFmt
    EF -->|creates| EL
    SF -->|creates| SS
    SF -->|creates| SFmt
    SF -->|creates| SL
    ES & SS -->|implement| AS
    EFmt & SFmt -->|implement| AFmt
    EL & SL -->|implement| AL

Why Object “Families” Matter #

The problem Abstract Factory solves is not about creating a single object — Factory Method already handles that. The problem is guaranteeing consistency between objects that must work together.

Consider the following scenario without Abstract Factory:

// ANTI-PATTERN: mixing objects from different families — nothing prevents this

type NotificationService struct {
    sender    Sender
    formatter Formatter
    logger    AuditLogger
}

func NewNotificationService() *NotificationService {
    return &NotificationService{
        sender:    &EmailSender{},     // from the Email family
        formatter: &SMSFormatter{},    // from the SMS family — inconsistent!
        logger:    &EmailAuditLogger{}, // from the Email family
    }
    // The compiler will not complain — but SMS-formatted text gets sent via Email
    // Bugs like this are extremely hard to debug
}

// CORRECT: the factory guarantees every object comes from the same family

func NewNotificationService(factory NotificationFactory) *NotificationService {
    return &NotificationService{
        sender:    factory.CreateSender(),
        formatter: factory.CreateFormatter(),
        logger:    factory.CreateAuditLogger(),
    }
    // Mixing is impossible — the factory is the only door
}

Inconsistencies like this can be very subtle in large systems. Abstract Factory removes the possibility structurally, not just by convention.


Structure and Components #

Abstract Factory has more components than Factory Method because it manages a family of products rather than a single product. Understanding each layer helps you read and write implementations correctly.

classDiagram
    class NotificationFactory {
        <<interface>>
        +CreateSender() Sender
        +CreateFormatter() Formatter
        +CreateAuditLogger() AuditLogger
    }

    class Sender {
        <<interface>>
        +Send(to, message string) error
    }

    class Formatter {
        <<interface>>
        +Format(template string, data map) string
        +FormatSubject(subject string) string
    }

    class AuditLogger {
        <<interface>>
        +Log(event NotificationEvent) error
    }

    class EmailFactory {
        +smtpHost string
        +apiKey string
        +CreateSender() Sender
        +CreateFormatter() Formatter
        +CreateAuditLogger() AuditLogger
    }

    class SMSFactory {
        +providerURL string
        +accountSID string
        +CreateSender() Sender
        +CreateFormatter() Formatter
        +CreateAuditLogger() AuditLogger
    }

    NotificationFactory <|.. EmailFactory
    NotificationFactory <|.. SMSFactory
    Sender <|.. EmailSender
    Sender <|.. SMSSender
    Formatter <|.. EmailFormatter
    Formatter <|.. SMSFormatter
    AuditLogger <|.. EmailAuditLogger
    AuditLogger <|.. SMSAuditLogger
    EmailFactory ..> EmailSender : creates
    EmailFactory ..> EmailFormatter : creates
    EmailFactory ..> EmailAuditLogger : creates
    SMSFactory ..> SMSSender : creates
    SMSFactory ..> SMSFormatter : creates
    SMSFactory ..> SMSAuditLogger : creates

The five components of Abstract Factory:

ComponentRoleExample
Abstract FactoryInterface defining all creation methodsNotificationFactory
Concrete FactoryImplementation that creates one product familyEmailFactory, SMSFactory
Abstract ProductInterface for each product typeSender, Formatter, AuditLogger
Concrete ProductConcrete implementation of each productEmailSender, SMSSender
ClientUses factory and products through interfaces onlyNotificationService

Full Implementation: Notification System #

Let’s build a notification system that supports Email and SMS. Each channel needs three components: a Sender to deliver messages, a Formatter to format the message, and an AuditLogger to record activity.

Abstract Products #

Define an interface for each product type the factory will create. This is the contract every concrete product must satisfy.

package notification

import "time"

// NotificationEvent represents one notification delivery event.
type NotificationEvent struct {
    Channel   string
    Recipient string
    Message   string
    SentAt    time.Time
    Success   bool
    Error     string
}

// Sender is responsible for delivering a message to the recipient.
type Sender interface {
    Send(recipient, message string) error
    ChannelName() string
}

// Formatter is responsible for formatting message content per channel.
type Formatter interface {
    FormatBody(template string, data map[string]string) string
    FormatSubject(subject string) string // relevant for email, ignored for SMS
}

// AuditLogger records all delivery activity for auditing purposes.
type AuditLogger interface {
    Log(event NotificationEvent) error
    GetLogs(recipient string) ([]NotificationEvent, error)
}

Abstract Factory #

One factory interface defining all products in a single family.

// NotificationFactory is the abstract factory — it defines the contract
// for creating an entire family of notification objects.
type NotificationFactory interface {
    CreateSender() Sender
    CreateFormatter() Formatter
    CreateAuditLogger() AuditLogger
}

Concrete Products: The Email Family #

package notification

import (
    "fmt"
    "net/smtp"
    "strings"
    "sync"
    "time"
)

// EmailSender delivers notifications over the SMTP protocol.
type EmailSender struct {
    smtpHost string
    smtpPort int
    username string
    password string
    fromAddr string
}

func (e *EmailSender) Send(recipient, message string) error {
    addr := fmt.Sprintf("%s:%d", e.smtpHost, e.smtpPort)
    auth := smtp.PlainAuth("", e.username, e.password, e.smtpHost)

    body := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: Notification\r\n\r\n%s",
        e.fromAddr, recipient, message)

    if err := smtp.SendMail(addr, auth, e.fromAddr, []string{recipient}, []byte(body)); err != nil {
        return fmt.Errorf("email send failed: %w", err)
    }
    return nil
}

func (e *EmailSender) ChannelName() string { return "email" }


// EmailFormatter formats messages with a structure suited to email.
type EmailFormatter struct{}

func (f *EmailFormatter) FormatBody(template string, data map[string]string) string {
    result := template
    for key, value := range data {
        result = strings.ReplaceAll(result, "{{"+key+"}}", value)
    }
    // Email can carry richer content — add a header and footer
    return fmt.Sprintf("Dear Customer,\n\n%s\n\nRegards,\nSupport Team", result)
}

func (f *EmailFormatter) FormatSubject(subject string) string {
    return fmt.Sprintf("[Notification] %s", subject)
}


// EmailAuditLogger records email delivery logs in an in-memory store.
// In production, this would write to a database or logging service.
type EmailAuditLogger struct {
    mu   sync.Mutex
    logs []NotificationEvent
}

func (l *EmailAuditLogger) Log(event NotificationEvent) error {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logs = append(l.logs, event)
    fmt.Printf("[EMAIL AUDIT] %s -> %s (success: %v)\n",
        event.SentAt.Format(time.RFC3339), event.Recipient, event.Success)
    return nil
}

func (l *EmailAuditLogger) GetLogs(recipient string) ([]NotificationEvent, error) {
    l.mu.Lock()
    defer l.mu.Unlock()
    var result []NotificationEvent
    for _, log := range l.logs {
        if log.Recipient == recipient {
            result = append(result, log)
        }
    }
    return result, nil
}

Concrete Products: The SMS Family #

// SMSSender delivers notifications through an SMS gateway.
type SMSSender struct {
    providerURL string
    accountSID  string
    authToken   string
    fromNumber  string
}

func (s *SMSSender) Send(recipient, message string) error {
    // In a real implementation: call the provider API (Twilio, Vonage, etc.)
    fmt.Printf("[SMS] Sending to %s via %s: %s\n", recipient, s.providerURL, message)
    return nil
}

func (s *SMSSender) ChannelName() string { return "sms" }


// SMSFormatter formats messages to fit SMS limits (160 characters per segment).
type SMSFormatter struct {
    maxLength int
}

func (f *SMSFormatter) FormatBody(template string, data map[string]string) string {
    result := template
    for key, value := range data {
        result = strings.ReplaceAll(result, "{{"+key+"}}", value)
    }
    // SMS needs no header/footer — short and to the point
    if len(result) > f.maxLength {
        return result[:f.maxLength-3] + "..."
    }
    return result
}

func (f *SMSFormatter) FormatSubject(subject string) string {
    // SMS has no subject — return an empty string
    return ""
}


// SMSAuditLogger records SMS delivery logs.
type SMSAuditLogger struct {
    mu   sync.Mutex
    logs []NotificationEvent
}

func (l *SMSAuditLogger) Log(event NotificationEvent) error {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.logs = append(l.logs, event)
    fmt.Printf("[SMS AUDIT] %s -> %s (success: %v)\n",
        event.SentAt.Format(time.RFC3339), event.Recipient, event.Success)
    return nil
}

func (l *SMSAuditLogger) GetLogs(recipient string) ([]NotificationEvent, error) {
    l.mu.Lock()
    defer l.mu.Unlock()
    var result []NotificationEvent
    for _, log := range l.logs {
        if log.Recipient == recipient {
            result = append(result, log)
        }
    }
    return result, nil
}

Concrete Factories #

Each factory gathers its configuration and creates its entire product family. All the configuration complexity stays hidden here — the client never needs to know these details.

// EmailConfig stores all the configuration the email factory needs.
type EmailConfig struct {
    SMTPHost string
    SMTPPort int
    Username string
    Password string
    FromAddr string
}

// EmailFactory creates the entire family of email-based notification objects.
type EmailFactory struct {
    config EmailConfig
    logger *EmailAuditLogger // shared logger within one factory instance
}

func NewEmailFactory(cfg EmailConfig) *EmailFactory {
    return &EmailFactory{
        config: cfg,
        logger: &EmailAuditLogger{},
    }
}

func (f *EmailFactory) CreateSender() Sender {
    return &EmailSender{
        smtpHost: f.config.SMTPHost,
        smtpPort: f.config.SMTPPort,
        username: f.config.Username,
        password: f.config.Password,
        fromAddr: f.config.FromAddr,
    }
}

func (f *EmailFactory) CreateFormatter() Formatter {
    return &EmailFormatter{}
}

func (f *EmailFactory) CreateAuditLogger() AuditLogger {
    return f.logger // all email senders share one logger
}


// SMSConfig stores all the configuration the SMS factory needs.
type SMSConfig struct {
    ProviderURL string
    AccountSID  string
    AuthToken   string
    FromNumber  string
    MaxLength   int
}

// SMSFactory creates the entire family of SMS-based notification objects.
type SMSFactory struct {
    config SMSConfig
    logger *SMSAuditLogger
}

func NewSMSFactory(cfg SMSConfig) *SMSFactory {
    maxLen := cfg.MaxLength
    if maxLen == 0 {
        maxLen = 160 // SMS standard
    }
    return &SMSFactory{
        config: cfg,
        logger: &SMSAuditLogger{},
    }
}

func (f *SMSFactory) CreateSender() Sender {
    return &SMSSender{
        providerURL: f.config.ProviderURL,
        accountSID:  f.config.AccountSID,
        authToken:   f.config.AuthToken,
        fromNumber:  f.config.FromNumber,
    }
}

func (f *SMSFactory) CreateFormatter() Formatter {
    return &SMSFormatter{maxLength: f.config.MaxLength}
}

func (f *SMSFactory) CreateAuditLogger() AuditLogger {
    return f.logger
}

Client Code #

The client uses NotificationFactory without ever naming a single concrete type. Switching from Email to SMS only requires swapping the injected factory.

// NotificationService is the client that uses the abstract factory.
type NotificationService struct {
    factory   NotificationFactory
    sender    Sender
    formatter Formatter
    logger    AuditLogger
}

func NewNotificationService(factory NotificationFactory) *NotificationService {
    return &NotificationService{
        factory:   factory,
        sender:    factory.CreateSender(),
        formatter: factory.CreateFormatter(),
        logger:    factory.CreateAuditLogger(),
    }
}

// Send delivers a notification and records the result in the audit log.
func (s *NotificationService) Send(recipient, template string, data map[string]string) error {
    body := s.formatter.FormatBody(template, data)

    err := s.sender.Send(recipient, body)

    event := NotificationEvent{
        Channel:   s.sender.ChannelName(),
        Recipient: recipient,
        Message:   body,
        SentAt:    time.Now(),
        Success:   err == nil,
    }
    if err != nil {
        event.Error = err.Error()
    }

    _ = s.logger.Log(event) // the audit trail is recorded even if sending fails

    return err
}

// SendWelcome is an example business method that knows nothing about channels.
func (s *NotificationService) SendWelcome(recipient, username string) error {
    return s.Send(
        recipient,
        "Welcome, {{name}}! Your account is now active.",
        map[string]string{"name": username},
    )
}

func (s *NotificationService) SendOTP(recipient, otp string) error {
    return s.Send(
        recipient,
        "Your OTP code is {{otp}}. It expires in 5 minutes.",
        map[string]string{"otp": otp},
    )
}

Usage from main.go:

func main() {
    // Configuration is read from the environment or a config file
    emailCfg := notification.EmailConfig{
        SMTPHost: "smtp.gmail.com",
        SMTPPort: 587,
        Username: os.Getenv("SMTP_USER"),
        Password: os.Getenv("SMTP_PASS"),
        FromAddr: "[email protected]",
    }

    smsCfg := notification.SMSConfig{
        ProviderURL: "https://api.twilio.com",
        AccountSID:  os.Getenv("TWILIO_SID"),
        AuthToken:   os.Getenv("TWILIO_TOKEN"),
        FromNumber:  "+628123456789",
        MaxLength:   160,
    }

    // Pick the factory in one place — the whole object family switches with it
    var factory notification.NotificationFactory
    if os.Getenv("NOTIFICATION_CHANNEL") == "sms" {
        factory = notification.NewSMSFactory(smsCfg)
    } else {
        factory = notification.NewEmailFactory(emailCfg)
    }

    svc := notification.NewNotificationService(factory)

    // Business code does not know whether this is email or SMS
    svc.SendWelcome("[email protected]", "Budi")
    svc.SendOTP("[email protected]", "847291")
}

Second Case Study: Cloud Provider Abstraction #

The notification system is a relatively simple example. Abstract Factory really shines in more complex scenarios like cloud provider abstraction, where a single “family” can consist of a dozen interdependent components.

// CloudFactory is the abstract factory for cloud resources.
// One factory represents one cloud provider — all its resources are compatible.
type CloudFactory interface {
    CreateObjectStorage() ObjectStorage
    CreateMessageQueue() MessageQueue
    CreateSecretManager() SecretManager
    CreateMetricsCollector() MetricsCollector
}

// ObjectStorage is the abstract product for object storage.
type ObjectStorage interface {
    Put(bucket, key string, data []byte) error
    Get(bucket, key string) ([]byte, error)
    Delete(bucket, key string) error
    ListKeys(bucket, prefix string) ([]string, error)
}

// MessageQueue is the abstract product for message queues.
type MessageQueue interface {
    Publish(topic string, payload []byte) error
    Subscribe(topic string, handler func([]byte) error) error
}

// SecretManager is the abstract product for secret management.
type SecretManager interface {
    GetSecret(name string) (string, error)
    SetSecret(name, value string) error
}

// MetricsCollector is the abstract product for metric delivery.
type MetricsCollector interface {
    Counter(name string, tags map[string]string) error
    Histogram(name string, value float64, tags map[string]string) error
}

With this structure, swapping the entire cloud infrastructure is just a matter of swapping the factory:

// In production: every resource from AWS
var factory CloudFactory = NewAWSFactory(awsConfig)

// In staging: every resource from GCP
var factory CloudFactory = NewGCPFactory(gcpConfig)

// In unit tests: every resource from mocks
var factory CloudFactory = NewMockCloudFactory()

// Business code does not change at all
app := NewApplication(factory)
app.Run()
flowchart LR
    subgraph Production
        AF1[AWSFactory] --> AS[S3Storage]
        AF1 --> AQ[SQSQueue]
        AF1 --> ASM[AWSSecretsManager]
        AF1 --> AM[CloudWatch]
    end

    subgraph Staging
        GF[GCPFactory] --> GS[GCSStorage]
        GF --> GQ[PubSubQueue]
        GF --> GSM[SecretManagerGCP]
        GF --> GM[StackdriverMetrics]
    end

    subgraph Testing
        MF[MockFactory] --> MS[InMemoryStorage]
        MF --> MQ[InMemoryQueue]
        MF --> MSM[InMemorySecrets]
        MF --> MM[NoopMetrics]
    end

    App[Application] -->|prod| AF1
    App -->|staging| GF
    App -->|test| MF

Abstract Factory vs Factory Method #

These two patterns are often confused because their names are similar and both deal with object creation. The difference is fundamental:

// Factory Method — one product, one method
type PaymentFactory interface {
    CreatePayment() Payment // one product
}

// Abstract Factory — a product family, many methods
type InfrastructureFactory interface {
    CreateStorage() Storage          // first product
    CreateQueue() MessageQueue       // second product
    CreateCache() Cache              // third product
    CreateLogger() DistributedLogger // fourth product
    // all of them must be compatible with each other
}

An easy way to tell them apart: if you only need one Create...(), use Factory Method. If you need several Create...() methods whose products must be compatible with each other, use Abstract Factory.

AspectFactory MethodAbstract Factory
Number of productsOneMany (a family)
Factory interfaceOne methodMany methods
Problem solvedFlexibility in creating a single objectConsistency between related objects
ComplexityLowerHigher
When to useVariations of one concept’s implementationVariations of an entire implementation stack
ExampleNewPayment(method)NewInfrastructure(provider)

Testing with a Mock Factory #

Abstract Factory makes testing very clean. You can create a MockFactory that returns mock products, and the whole test suite runs without external dependencies.

// MockNotificationFactory for testing — replaces the Email or SMS factory
type MockNotificationFactory struct {
    sender    *MockSender
    formatter *MockFormatter
    logger    *MockAuditLogger
}

func NewMockNotificationFactory() *MockNotificationFactory {
    return &MockNotificationFactory{
        sender:    &MockSender{},
        formatter: &MockFormatter{},
        logger:    &MockAuditLogger{},
    }
}

func (f *MockNotificationFactory) CreateSender() Sender       { return f.sender }
func (f *MockNotificationFactory) CreateFormatter() Formatter { return f.formatter }
func (f *MockNotificationFactory) CreateAuditLogger() AuditLogger { return f.logger }

// MockSender records calls for assertion in tests
type MockSender struct {
    SentMessages []struct{ Recipient, Message string }
    ShouldFail   bool
}

func (m *MockSender) Send(recipient, message string) error {
    if m.ShouldFail {
        return errors.New("mock send failure")
    }
    m.SentMessages = append(m.SentMessages, struct{ Recipient, Message string }{recipient, message})
    return nil
}
func (m *MockSender) ChannelName() string { return "mock" }

// MockFormatter returns a predictable string for assertions
type MockFormatter struct{}

func (f *MockFormatter) FormatBody(template string, data map[string]string) string {
    return template // return the template as-is for testing
}
func (f *MockFormatter) FormatSubject(subject string) string { return subject }

// MockAuditLogger records logs for assertion in tests
type MockAuditLogger struct {
    Events []NotificationEvent
}

func (l *MockAuditLogger) Log(event NotificationEvent) error {
    l.Events = append(l.Events, event)
    return nil
}
func (l *MockAuditLogger) GetLogs(recipient string) ([]NotificationEvent, error) {
    return l.Events, nil
}


// Clean tests — no SMTP, no network
func TestNotificationService_SendWelcome(t *testing.T) {
    mockFactory := NewMockNotificationFactory()
    svc := NewNotificationService(mockFactory)

    err := svc.SendWelcome("[email protected]", "Budi")

    if err != nil {
        t.Fatalf("expected no error, got: %v", err)
    }

    // Assert the message was sent
    if len(mockFactory.sender.SentMessages) != 1 {
        t.Errorf("expected 1 message sent, got %d", len(mockFactory.sender.SentMessages))
    }
    if mockFactory.sender.SentMessages[0].Recipient != "[email protected]" {
        t.Errorf("wrong recipient")
    }

    // Assert the audit log was recorded
    if len(mockFactory.logger.Events) != 1 {
        t.Errorf("expected 1 audit event, got %d", len(mockFactory.logger.Events))
    }
    if !mockFactory.logger.Events[0].Success {
        t.Errorf("expected success=true in audit log")
    }
}

func TestNotificationService_SendWelcome_WhenSenderFails(t *testing.T) {
    mockFactory := NewMockNotificationFactory()
    mockFactory.sender.ShouldFail = true
    svc := NewNotificationService(mockFactory)

    err := svc.SendWelcome("[email protected]", "Budi")

    if err == nil {
        t.Error("expected error when sender fails")
    }

    // The audit log is still recorded even on failure
    if len(mockFactory.logger.Events) != 1 {
        t.Errorf("expected audit log even on failure")
    }
    if mockFactory.logger.Events[0].Success {
        t.Errorf("expected success=false in audit log")
    }
}

Don’t Create a Factory That Is Too Large

If your abstract factory has more than 5-7 Create...() methods, that is a strong signal the factory is trying to do too much. Consider splitting it into several smaller, focused factories — for instance, a separate StorageFactory and MessagingFactory instead of one monolithic InfrastructureFactory.


When to Use and When Not to #

Abstract Factory is a powerful pattern, but it is not the answer to every problem. Overusing it produces complex code without real benefit.

USE Abstract Factory if:
  ✓ There are several implementation "families" that will be swapped between
  ✓ Objects within one family must be compatible and consistent
  ✓ The client must not know concrete classes at all
  ✓ There is a real chance of adding new families in the future
  ✓ System testing needs the whole family mocked at once

AVOID Abstract Factory if:
  ✗ There is only one implementation and no plan to add more
  ✗ Products in the "family" do not depend on each other
  ✗ The abstraction complexity outweighs the benefit
  ✗ Small team with a tight timeline — start simple, refactor when needed
  ✗ Every factory method has only one concrete implementation

Start Simple, Refactor When Needed

If you are not sure you need Abstract Factory, start with a Simple Factory or even plain constructors. When you start seeing the pattern of “a set of objects that must be swapped together” — that is the right time to refactor to Abstract Factory. Premature abstraction is more dangerous than refactoring later.


Abstract Factory Review Checklist #

DESIGN:
  □ There is an abstract factory interface with more than one Create method
  □ All products in one family are compatible with each other
  □ The client depends only on the abstract factory and abstract products
  □ There is no way to mix products from different families

IMPLEMENTATION:
  □ Each concrete factory creates its entire product family
  □ Configuration is injected into the factory, not into concrete products
  □ The factory contains no business logic — only object creation
  □ Errors from the creation process are handled clearly

CONSISTENCY:
  □ Every concrete factory implements all the methods in the interface
  □ Products from one factory work together without extra configuration
  □ Adding a new family does not change client code at all

TESTING:
  □ There is a mock factory that returns mock products
  □ The test suite runs without external dependencies
  □ Edge cases (factory failure, products returning errors) are tested
  □ The audit trail (logger) is tested even for failure scenarios

Summary #

  • Abstract Factory manages object families — unlike Factory Method, which manages a single object, here one factory is responsible for many products that must be compatible.
  • Consistency is the main advantage — there is no way to accidentally mix products from different families; an inconsistency like EmailSender with SMSFormatter becomes structurally impossible.
  • Five components: Abstract Factory, Concrete Factory, Abstract Product, Concrete Product, and Client — each with a clear, non-overlapping role.
  • The client is fully isolated from concrete classes; swapping an entire implementation family is just a matter of swapping the injected factory — no other code changes.
  • Testing becomes very clean — a Mock Factory replaces the entire product family at once; no SMTP, no HTTP calls, no database.
  • Distinguish it from Factory Method: use Factory Method for one product with implementation variations; use Abstract Factory for several products that must stay consistent within one family.
  • Don’t over-engineer — start with plain constructors or Factory Method, and refactor to Abstract Factory only when a real need to swap entire object families appears.
  • A large factory is a danger signal — more than 5-7 Create...() methods means the factory is trying to do too much; split it into several more focused factories.

← Previous: Factory Method   Next: Builder →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact