Abstract Factory — families of related objects
Concept
The Abstract Factory pattern provides an interface for creating families of related objects without specifying their concrete classes. Where Factory Method creates one product type, Abstract Factory creates a suite of related products that are designed to work together. The client code uses only the abstract factory interface and abstract product interfaces — it never sees the concrete classes.
The problem it solves is cross-product consistency. Imagine building a notification system that supports multiple channels: email, SMS, and push notifications. Each channel has its own sender, template renderer, and delivery tracker. If you mix implementations — say, a SendGrid email sender with an SMS template renderer — you get a mismatched, broken system. Abstract Factory guarantees that all components come from the same family. You ask the factory for a sender, a renderer, and a tracker, and you know they will all be compatible.
UML structure: An AbstractFactory interface declares creation methods for each product type (createSender(), createRenderer(), createTracker()). Concrete factories (EmailNotificationFactory, SmsNotificationFactory) implement all those methods and return concrete products that belong together. Each product type has its own interface (SenderInterface, RendererInterface), and concrete products implement these. Client code works only with the abstract factory and abstract product interfaces.
When to use: when the system must be independent of how products are created; when you need to enforce consistency among related products; when a product family should be used together. When NOT to use: when you only have one product family (use Factory Method instead); when adding a new product type to the family is frequent (requires updating all concrete factories — this is the Abstract Factory's main weakness). It is particularly heavy if product types vary independently rather than as a family.
Laravel's Illuminate\Database layer uses an Abstract Factory approach in ConnectionFactory, which produces Connector + Connection pairs per driver. More visibly, the FilesystemManager creates driver-specific adapters and filesystems together — a family of collaborating objects for each disk driver (local, S3, FTP).
Code Example
<?php
declare(strict_types=1);
// ── Abstract Products ───────────────────────────────────────────────────────
interface NotificationSenderInterface
{
public function send(string $recipient, string $content): DeliveryReceipt;
}
interface NotificationRendererInterface
{
public function render(NotificationTemplate $template, array $data): string;
}
interface DeliveryTrackerInterface
{
public function track(DeliveryReceipt $receipt): DeliveryStatus;
}
// ── Abstract Factory ────────────────────────────────────────────────────────
interface NotificationChannelFactory
{
public function createSender(): NotificationSenderInterface;
public function createRenderer(): NotificationRendererInterface;
public function createTracker(): DeliveryTrackerInterface;
}
// ── Concrete Family: Email ──────────────────────────────────────────────────
final class SendGridEmailSender implements NotificationSenderInterface
{
public function __construct(private readonly string $apiKey) {}
public function send(string $recipient, string $content): DeliveryReceipt
{
// SendGrid API call
return new DeliveryReceipt(id: 'sg_' . uniqid(), channel: 'email');
}
}
final class HtmlEmailRenderer implements NotificationRendererInterface
{
public function render(NotificationTemplate $template, array $data): string
{
return view($template->viewPath(), $data)->render();
}
}
final class SendGridDeliveryTracker implements DeliveryTrackerInterface
{
public function track(DeliveryReceipt $receipt): DeliveryStatus
{
// Query SendGrid webhooks/events API
return DeliveryStatus::delivered();
}
}
final class EmailNotificationFactory implements NotificationChannelFactory
{
public function createSender(): NotificationSenderInterface
{
return new SendGridEmailSender(apiKey: config('services.sendgrid.key'));
}
public function createRenderer(): NotificationRendererInterface
{
return new HtmlEmailRenderer();
}
public function createTracker(): DeliveryTrackerInterface
{
return new SendGridDeliveryTracker();
}
}
// ── Concrete Family: SMS ────────────────────────────────────────────────────
final class TwilioSmsSender implements NotificationSenderInterface
{
public function __construct(
private readonly string $accountSid,
private readonly string $authToken,
private readonly string $fromNumber,
) {}
public function send(string $recipient, string $content): DeliveryReceipt
{
// Twilio API call
return new DeliveryReceipt(id: 'SM' . uniqid(), channel: 'sms');
}
}
final class PlainTextSmsRenderer implements NotificationRendererInterface
{
public function render(NotificationTemplate $template, array $data): string
{
// SMS gets a plain text version — strips HTML, enforces 160 char limit per segment
return substr(strip_tags(view($template->textPath(), $data)->render()), 0, 1600);
}
}
final class TwilioDeliveryTracker implements DeliveryTrackerInterface
{
public function track(DeliveryReceipt $receipt): DeliveryStatus
{
return DeliveryStatus::delivered();
}
}
final class SmsNotificationFactory implements NotificationChannelFactory
{
public function createSender(): NotificationSenderInterface
{
return new TwilioSmsSender(
accountSid: config('services.twilio.sid'),
authToken: config('services.twilio.token'),
fromNumber: config('services.twilio.from'),
);
}
public function createRenderer(): NotificationRendererInterface
{
return new PlainTextSmsRenderer();
}
public function createTracker(): DeliveryTrackerInterface
{
return new TwilioDeliveryTracker();
}
}
// ── Client code — works only with abstractions ──────────────────────────────
final class NotificationDispatcher
{
public function __construct(
private readonly NotificationChannelFactory $factory,
) {}
public function dispatch(User $user, NotificationTemplate $template, array $data): DeliveryStatus
{
$sender = $this->factory->createSender();
$renderer = $this->factory->createRenderer();
$tracker = $this->factory->createTracker();
$content = $renderer->render($template, $data);
$receipt = $sender->send($user->notificationRecipient(), $content);
return $tracker->track($receipt);
}
}
// ── Service Provider wiring ─────────────────────────────────────────────────
$this->app->bind(NotificationChannelFactory::class, function (): NotificationChannelFactory {
return match (config('notifications.default_channel')) {
'email' => new EmailNotificationFactory(),
'sms' => new SmsNotificationFactory(),
default => throw new \InvalidArgumentException('Unknown notification channel'),
};
});Interview Q&A
Q: What is the key difference between Abstract Factory and Factory Method, and how do you decide which to use?
Factory Method is about one product: you define a single creation method and subclasses decide what concrete type to return. Abstract Factory is about a family of products: the factory declares creation methods for each product in the family, ensuring they are compatible. Choose Factory Method when you have a single type of product and need subclasses to vary the creation. Choose Abstract Factory when you have multiple product types that must be used together and you want to prevent mixing incompatible families — for example, you never want a SendGrid sender with a Twilio tracker. The tradeoff: Abstract Factory is harder to extend with new product types (you must update every concrete factory), while Factory Method is hard to extend with new product families (you need a new subclass for every variant).
Q: How does Laravel's notification system relate to Abstract Factory?
Laravel's built-in notification channels do not strictly implement Abstract Factory as a class hierarchy, but the concept is present in Illuminate\Notifications\ChannelManager. Each channel driver (mail, nexmo, slack, database) bundles its own set of collaborating classes: a MailChannel uses a Mailer and renders MailMessage objects; a DatabaseChannel creates DatabaseNotification Eloquent models. The ChannelManager acts as the factory that produces the correct channel family based on the channel name returned by $notification->via(). The NotificationFake in tests replaces the entire factory, which is exactly the testability benefit Abstract Factory promises.
Q: What is the main disadvantage of Abstract Factory and how do teams work around it?
Adding a new product type to an existing Abstract Factory hierarchy requires modifying every concrete factory — a violation of the Open/Closed Principle. For example, if you add a createLogger() method to NotificationChannelFactory, every existing factory class (EmailNotificationFactory, SmsNotificationFactory, and any third-party implementations) must be updated. Teams work around this with default implementations in the interface (if using PHP 8+ interface default methods, though that is controversial), or more commonly by providing an abstract base class with a sensible default for the new product type. Another approach is composition — instead of an Abstract Factory interface, use a value object or DTO that holds all the products, built once by a simple factory function. This sidesteps the hierarchy at the cost of compile-time safety.