In our earlier discussions, we talked about SOLID, SDLC, and PSR. Those topics help us write better code, follow proper process, and keep our project clean and professional.
Now the next step is Design Patterns.
Design patterns are very important in real software development. They help us solve common programming problems in a smart and reusable way. When you understand design patterns, your PHP code becomes more flexible, readable, and easier to maintain.
In simple words:
A design pattern is a proven way to solve a common software problem.
It is not a framework.
It is not a library.
It is not copy-paste code.
It is a smart coding approach that experienced developers use again and again.
What is a Design Pattern?
A design pattern is like a recipe.
When you want to cook something, you do not start from zero every time. You follow a known method. In the same way, when we solve software problems, we can use known patterns.
For example:
- If you need one common object for the whole project, you may use Singleton
- If you need to create objects based on a condition, you may use Factory
- If you need to switch behavior dynamically, you may use Strategy
- If you need one action to inform many other parts, you may use Observer
These patterns save time and improve code quality.
Why Design Patterns Matter
A lot of beginners write code that works, but later it becomes hard to change.
That usually happens when code is too tightly coupled, too long, or too mixed together.
Design patterns help us:
- keep code organized
- reduce duplication
- make code easier to extend
- improve readability
- separate responsibilities properly
- make testing easier
As a senior developer, I always think like this:
Code should not only work today. It should also be easy to change tomorrow.
That is where design patterns help.
Types of Design Patterns
Design patterns are usually grouped into three main categories:
1. Creational Patterns
These patterns help us create objects in a better way.
Example: Factory, Singleton
2. Structural Patterns
These patterns help us build class structure and object relationships.
Example: Adapter, Decorator
3. Behavioral Patterns
These patterns help us manage how objects communicate and behave.
Example: Strategy, Observer
In this article, I will explain the most practical patterns with simple PHP examples that a junior developer or student can understand easily.
1) Singleton Pattern
Meaning
Singleton means only one instance of a class should exist in the whole application.
This pattern is useful when you want one shared object, like:
- configuration
- logger
- database connection manager
- app settings
But one important thing: Singleton should be used carefully. Do not use it everywhere. Use it only when it really makes sense.
Simple Example
<?php
class Config
{
private static ?Config $instance = null;
private array $settings = [];
// Private constructor prevents direct object creation
private function __construct()
{
$this->settings = [
'app_name' => 'My PHP App',
'version' => '1.0.0'
];
}
// This method returns the same single instance every time
public static function getInstance(): Config
{
if (self::$instance === null) {
self::$instance = new Config();
}
return self::$instance;
}
public function get(string $key): mixed
{
return $this->settings[$key] ?? null;
}
}
// Use the singleton
$config1 = Config::getInstance();
$config2 = Config::getInstance();
echo $config1->get('app_name') . PHP_EOL;
echo $config2->get('version') . PHP_EOL;
// Both are same object
var_dump($config1 === $config2);Explanation
Here:
private __construct()stops direct object creationgetInstance()gives the only object- the same instance is reused everywhere
So if different parts of the application need the same config data, they can use the same object.
When to use
Use Singleton when:
- one object should exist only once
- you need a global shared access point
- the object represents application-wide state
Do not use Singleton just because it looks advanced.
2) Factory Pattern
Meaning
Factory pattern is used when we want to create objects without directly using new everywhere.
Instead of asking the code to know every class name, we let a factory decide which object should be created.
This is very useful when object creation depends on a condition.
For example:
- email notification
- SMS notification
- WhatsApp notification
Simple Example
<?php
interface NotificationInterface
{
public function send(string $message): void;
}
class EmailNotification implements NotificationInterface
{
public function send(string $message): void
{
echo "Sending Email: " . $message . PHP_EOL;
}
}
class SmsNotification implements NotificationInterface
{
public function send(string $message): void
{
echo "Sending SMS: " . $message . PHP_EOL;
}
}
class NotificationFactory
{
public static function make(string $type): NotificationInterface
{
return match ($type) {
'email' => new EmailNotification(),
'sms' => new SmsNotification(),
default => throw new Exception("Unsupported notification type"),
};
}
}
// Client code
$notification = NotificationFactory::make('email');
$notification->send('Your order has been placed.');
$notification2 = NotificationFactory::make('sms');
$notification2->send('Your OTP is 123456.');Explanation
In this example:
NotificationInterfacedefines a common behaviorEmailNotificationandSmsNotificationfollow that contractNotificationFactorycreates the correct object- main code does not need to know internal class details
This makes the code cleaner and easier to extend.
Why Factory is useful
If tomorrow you add:
- WhatsAppNotification
- PushNotification
- TelegramNotification
you only update the factory and add a new class.
That is much better than writing if and switch logic everywhere.
3) Strategy Pattern
Meaning
Strategy pattern is used when you want to change behavior at runtime.
In simple words:
Same task, different methods.
For example, payment can be done by:
- credit card
- UPI
- cash on delivery
- wallet
The main process stays the same, but the payment method changes.
Simple Example
<?php
interface PaymentMethodInterface
{
public function pay(float $amount): void;
}
class CardPayment implements PaymentMethodInterface
{
public function pay(float $amount): void
{
echo "Paid $" . $amount . " using Card" . PHP_EOL;
}
}
class UpiPayment implements PaymentMethodInterface
{
public function pay(float $amount): void
{
echo "Paid $" . $amount . " using UPI" . PHP_EOL;
}
}
class CashPayment implements PaymentMethodInterface
{
public function pay(float $amount): void
{
echo "Paid $" . $amount . " using Cash" . PHP_EOL;
}
}
class PaymentProcessor
{
public function __construct(private PaymentMethodInterface $method)
{
}
public function process(float $amount): void
{
$this->method->pay($amount);
}
}
// Use different strategies
$cardPayment = new PaymentProcessor(new CardPayment());
$cardPayment->process(1500);
$upiPayment = new PaymentProcessor(new UpiPayment());
$upiPayment->process(999);
$cashPayment = new PaymentProcessor(new CashPayment());
$cashPayment->process(500);Explanation
Here:
PaymentMethodInterfaceis the common contract- each payment class is one strategy
PaymentProcessordoes not care which payment type is used
This is a very clean design.
Why Strategy is powerful
Without Strategy, people usually write big if-else blocks like this:
- if payment is card
- else if payment is UPI
- else if payment is cash
That works for small code, but later it becomes messy.
Strategy removes that mess and keeps each behavior in its own class.
4) Observer Pattern
Meaning
Observer pattern is used when one object changes and many other objects need to react.
A good real-world example is order placement.
When an order is placed, many things may happen:
- send email
- send SMS
- update stock
- write log
- notify admin
Instead of putting all logic inside one class, we can notify observers.
Simple Example
<?php
interface ObserverInterface
{
public function update(string $event): void;
}
class EmailObserver implements ObserverInterface
{
public function update(string $event): void
{
echo "Email notification sent for event: " . $event . PHP_EOL;
}
}
class SmsObserver implements ObserverInterface
{
public function update(string $event): void
{
echo "SMS notification sent for event: " . $event . PHP_EOL;
}
}
class OrderService
{
private array $observers = [];
public function attach(ObserverInterface $observer): void
{
$this->observers[] = $observer;
}
public function placeOrder(string $orderId): void
{
echo "Order placed: " . $orderId . PHP_EOL;
$this->notify("order_placed");
}
private function notify(string $event): void
{
foreach ($this->observers as $observer) {
$observer->update($event);
}
}
}
// Create order service
$orderService = new OrderService();
// Attach observers
$orderService->attach(new EmailObserver());
$orderService->attach(new SmsObserver());
// Place order
$orderService->placeOrder("ORD-1001");Explanation
Here:
OrderServiceis the main subjectEmailObserverandSmsObserverlisten for events- when order is placed, all observers get notified
This keeps the order system clean.
Why Observer is useful
It helps us avoid tight coupling.
The order system should not know too much about email or SMS details.
It should only notify interested parts.
That is a very good real-world design.
5) Adapter Pattern
Meaning
Adapter pattern helps two incompatible parts work together.
Imagine you have old code and new code, but their interfaces do not match. Adapter solves this by wrapping one class into another shape.
This is common in real projects when integrating:
- old APIs
- payment gateways
- third-party services
Simple Example
<?php
class OldPaymentGateway
{
public function makePayment(int $rupees): void
{
echo "Paid Rs. " . $rupees . " using old gateway" . PHP_EOL;
}
}
interface NewPaymentInterface
{
public function pay(float $amount): void;
}
class OldPaymentAdapter implements NewPaymentInterface
{
public function __construct(private OldPaymentGateway $gateway)
{
}
public function pay(float $amount): void
{
// Convert float amount to integer rupees
$this->gateway->makePayment((int) $amount);
}
}
class CheckoutService
{
public function __construct(private NewPaymentInterface $payment)
{
}
public function checkout(float $amount): void
{
$this->payment->pay($amount);
}
}
$adapter = new OldPaymentAdapter(new OldPaymentGateway());
$checkout = new CheckoutService($adapter);
$checkout->checkout(499.99);Explanation
Here:
- old gateway has one method
- new code expects another interface
- adapter connects both
This is very practical when you cannot change old code but still need to use it.
How I think about Design Patterns in real projects
As a developer, I do not use design patterns just to show knowledge.
I use them only when they solve a real problem.
That is the correct way.
A junior developer often asks:
“Which pattern should I use?”
My answer is:
First understand the problem. Then choose the pattern.
Do not force a pattern into every class.
Patterns are tools, not rules.
Common mistakes beginners make
1. Using patterns too early
Do not add design patterns just because they sound smart.
2. Making code too complex
A simple if-else is fine when the problem is simple.
3. Mixing responsibilities
One class should not do everything.
4. Wrong pattern selection
Factory and Strategy look similar sometimes, but they solve different problems.
5. Not understanding the business need
Always understand the actual use case first.
Easy Summary of Important Patterns
Singleton
One object only.
Factory
Create objects in a smart way.
Strategy
Change behavior at runtime.
Observer
One event notifies many listeners.
Adapter
Make incompatible code work together.
Final Thoughts
Design patterns are one of the most useful topics in software development.
They help us write code that is:
- cleaner
- easier to maintain
- easier to extend
- easier to test
- easier for team members to understand
If you are a junior developer or student, do not try to memorize every pattern at once.
First understand the problem each pattern solves.
Then practice small examples like the ones above.
That is the best way to learn.
And if you are writing PHP professionally, design patterns will help you build better software for the long run.