Abstract Factory
Abstract Factory creates a family of related objects. Unlike Factory Method, which varies one product, a factory here guarantees that several products belong to the same variant.
iOS problem and when to use it
A checkout flow can be consumer or business. Each variant needs both a payment form and a receipt formatter. Building them independently risks pairing a business form with a consumer receipt. Use an Abstract Factory when these products must change together. If only one product varies, a simple factory is enough.
Original example: matching checkout components
import Foundation
protocol PaymentForm {
var requiredFields: [String] { get }
}
protocol ReceiptFormatter {
func title(for orderID: String) -> String
}
protocol CheckoutComponentFactory {
func makeForm() -> any PaymentForm
func makeReceiptFormatter() -> any ReceiptFormatter
}
struct ConsumerForm: PaymentForm {
let requiredFields = ["Card number", "Postal code"]
}
struct BusinessForm: PaymentForm {
let requiredFields = ["Purchase order", "Tax ID"]
}
struct ConsumerReceipt: ReceiptFormatter {
func title(for orderID: String) -> String { "Order \(orderID)" }
}
struct BusinessReceipt: ReceiptFormatter {
func title(for orderID: String) -> String { "Invoice for \(orderID)" }
}
struct ConsumerCheckoutFactory: CheckoutComponentFactory {
func makeForm() -> any PaymentForm { ConsumerForm() }
func makeReceiptFormatter() -> any ReceiptFormatter { ConsumerReceipt() }
}
struct BusinessCheckoutFactory: CheckoutComponentFactory {
func makeForm() -> any PaymentForm { BusinessForm() }
func makeReceiptFormatter() -> any ReceiptFormatter { BusinessReceipt() }
}
func checkoutSummary(using factory: any CheckoutComponentFactory) -> String {
let fields = factory.makeForm().requiredFields.joined(separator: ", ")
return "\(factory.makeReceiptFormatter().title(for: "42")): \(fields)"
}The app chooses one factory after account type is known; the checkout feature receives that factory and never mixes product families. In UIKit or SwiftUI, the form model drives fields and the formatter drives receipt text.
Production decisions
Choose the factory at the app or flow composition root. Test that each variant creates a compatible pair. If you add a new product requirement to CheckoutComponentFactory, every family must implement it, so keep the family limited to objects that truly vary together. Localization belongs in the formatter implementation, not in the view.
Why the pieces are arranged this way
PaymentForm and ReceiptFormatter are separate products: one describes input and the other describes output. CheckoutComponentFactory promises both. The concrete consumer and business factories are the only places that know which pair belongs together. checkoutSummary(using:) receives the abstract factory, so it cannot accidentally reach for a product from the other family. This is the central guarantee of the pattern, not just a way to shorten initializers.
From example to an iOS screen
At login or checkout entry, determine the account variant and create one factory. Pass it to a checkout coordinator or view model. The view model can ask the factory for a form model; a SwiftUI view renders the required fields or a UIKit controller builds rows. After a successful order, the same factory produces the receipt formatter. If the account changes while the flow is open, decide whether to restart the flow with a new factory or freeze the original variant for that order. Mixing factories mid-flow defeats the guarantee.
Failure modes and verification
A new family requires implementations for all products. That work is intentional, but it becomes cumbersome if products do not actually vary together. Test each factory as a pair, including localized text and required fields, and test that the composition root selects the right family. Factory Method varies one creation step; Abstract Factory varies a related family.