Back to blog

Software design

Software Design Patterns: Solve the Repeated Problem, Not the Diagram

Learn how software design principles and Strategy, Factory, Adapter and Observer patterns support safer change without needless abstraction.

Design principles help a team judge whether code is easy to change. Design patterns give names to recurring solution shapes. The useful skill is knowing which problem deserves a pattern and when the simple version should stay simple.

01

The awkward code usually arrives one sensible change at a time

A business application starts with one way to send a customer update. The code checks a case, builds an email and sends it through the chosen provider. There is no reason to make it more complicated.

Then customers ask for text messages. Some updates need an attachment. A new supplier uses different field names. Marketing wants a copy of one event. Operations needs an audit trail, retries and a visible failure state.

None of those requests is unreasonable. The problem appears when every new variation adds another conditional to the same class. Sending a message becomes mixed with choosing a channel, formatting data, calling a supplier, recording an audit entry and deciding what to do when the supplier is unavailable.

That is the point where design patterns and principles become useful. They are not decorations to add before the first release. They are ways to make a repeated design problem easier to discuss and safer to change.

The goal is not to prove that the code contains Strategy, Factory and Observer. The goal is to stop a small change in one area creating a surprise somewhere else.

02

Principles and patterns do different jobs

A design principle is a guide for judging a design. Separation of concerns asks whether different kinds of work have been kept apart. Single responsibility asks whether a class or module has one coherent reason to change. Dependency inversion asks whether important business rules depend directly on replaceable technical details.

A design pattern is a named solution shape for a problem that occurs repeatedly. Strategy gives interchangeable ways to perform the same behaviour. Adapter translates an awkward external interface into one the application understands. Observer allows several interested parts of a system to react to an event.

The distinction matters. A principle can reveal that supplier code is leaking into business rules. A pattern may then offer a useful structure for fixing it. Starting with the pattern name works backwards. It encourages the team to find somewhere to install a familiar diagram, whether the problem exists or not.

Sheffield Hallam University's overview makes the same broad distinction: principles provide guidance while patterns provide reusable approaches to recurring problems. The useful next step is to connect those ideas to an actual change the software must support.

03

Start with the change pressure

Before choosing a pattern, write one plain sentence about the pressure on the code.

For the notification example, the first useful sentence might be: the system needs to send the same business update through different channels without the case workflow knowing the details of each provider.

That sentence contains more design information than a list of pattern names. It identifies what must stay stable, what is likely to vary and which knowledge should not spread through the application.

Look for evidence in recent changes. Which conditional keeps growing? Which supplier detail appears in several places? Which class changes when unrelated teams make requests? Which test needs half the application to be running before it can check one rule?

A pattern earns its extra structure when it reduces a repeated cost. If the application still sends one kind of email through one provider and no second variation is credible, a direct implementation may remain the clearest design.

04

Separate the business decision from the plumbing

The case workflow should decide that a customer needs an update. It should not also know how an email supplier authenticates, how a text message is formatted or where delivery attempts are stored.

This is separation of concerns in ordinary work. The business rule, message content, transport and operational record are different concerns because they change for different reasons.

Single responsibility does not mean every class must contain one tiny method. It means a unit should have a coherent purpose and a recognisable owner. A notification policy may decide whether an update is allowed. A formatter may build the content. A channel implementation may deliver it. An audit component may record the result.

Microsoft's architectural guidance connects this separation to testability and change. Core business behaviour should not be tightly coupled to infrastructure or user interface details. That does not require a complicated architecture. It requires a boundary that keeps the important rule from absorbing every technical concern around it.

05

Use dependency inversion to protect the important rule

Dependency inversion sounds academic until an external service changes its API on a Friday afternoon.

Without a boundary, the case workflow may call a concrete email client directly. Its tests need supplier configuration, its error handling understands supplier exceptions and its data structures begin to mirror the supplier's request format.

A small application owned interface changes the direction of that dependency. The workflow asks a notification channel to deliver a message. The email provider implementation agrees to that interface. The high level rule no longer needs to know which SDK happens to perform the work.

This also makes dependencies explicit. A class that needs a notifier should receive one rather than reach into global state and hope the right service has been configured. The code becomes easier to understand and a focused test can supply a controlled implementation.

An interface is not valuable merely because it exists. If it copies every method from one supplier, the application still depends on that supplier's design. The boundary should express what the business needs, not provide a new home for the vendor's vocabulary.

06

Strategy fits when the same job has several valid methods

Strategy is useful when the application performs one recognisable job in several interchangeable ways.

Email, text message and in-app notification can each implement the same delivery contract. The case workflow selects the appropriate channel from customer preferences and asks it to deliver the update. It does not contain the individual sending algorithms.

The benefit is not the removal of every conditional. A decision still needs to choose the strategy. The useful change is where that decision lives and how far each implementation can spread. Adding a new channel should mainly mean adding one implementation and one explicit selection rule.

Strategy is less useful when the behaviours are not genuinely interchangeable. Posting an invoice to an accounting system and sending a customer message may both involve an API call, but they are different business actions with different data, failure rules and ownership. Forcing them behind one generic integration strategy would hide important differences.

07

Factory helps when construction becomes a decision

Once several notification strategies exist, something must create the right one. That may involve configuration, credentials, tenant settings or feature availability.

A factory can keep that construction decision in one place. The rest of the application asks for the channel associated with a known type instead of creating supplier clients throughout controllers, jobs and commands.

This does not mean every call to a constructor needs a factory. Moving one obvious construction line into a new class can add a name without removing any difficulty. The pattern earns its place when creation is conditional, repeated, expensive to configure or likely to change independently from the code that uses the result.

The important outcome is controlled construction. A developer can see which implementations are available, where configuration is applied and what happens when a requested channel is unsupported.

08

Adapter keeps supplier differences at the edge

Third party services rarely share the interface your application would have designed for itself. One provider calls the recipient a destination, another expects a phone number, and a legacy service returns a success code that needs interpretation.

An Adapter translates between that external interface and the small application interface. The business code deals with a delivery result it understands. The adapter deals with authentication, request formats, response codes and supplier specific errors.

This boundary is particularly useful during a replacement. A new provider can receive its own adapter while the rest of the application continues to use the same contract. Both implementations can be tested against the behaviours the application relies on.

An adapter does not make suppliers identical. Rate limits, delivery guarantees and outage behaviour still need honest treatment. The pattern keeps those differences visible at a controlled edge instead of allowing them to leak everywhere.

09

Observer can spread a useful event, and hide a messy flow

When a case is approved, several parts of the system may need to react. The certificate is generated, the customer is notified, an audit entry is written and reporting is updated.

Observer, often implemented through events and listeners, allows those reactions to be registered without the approval service directly calling every concrete component. That can keep the central action focused and make new reactions easier to add.

The trade-off is visibility. A developer reading the approval method may no longer see everything that happens afterwards. Listener order, retries and partial failures can become difficult to reason about. An event name that merely says case updated also leaves subscribers guessing which change occurred.

Use events for meaningful facts that have already happened, such as case approved. Record enough context to diagnose the flow, make failure behaviour explicit and keep essential transactional work close enough to the original action that consistency is protected. Observer reduces direct coupling. It does not remove the need to understand the whole business process.

10

Match the symptom to the smallest useful response

Patterns overlap because real code problems overlap. The name matters less than the pressure being relieved and the cost introduced.

Design symptoms, principles and possible patterns
Design symptomPrinciple to checkPossible patternCost to accept
One operation has several interchangeable methodsSeparate varying behaviourStrategyMore types and an explicit selection rule
Object creation depends on configuration or contextKeep construction separate from useFactoryAnother place to navigate and test
A supplier interface leaks through business codeDependency inversion and separation of concernsAdapterTranslation code must be maintained
Several parts react to one completed actionKeep the publisher independent from optional reactionsObserver or domain eventThe flow becomes less visible
A process has a stable sequence with a few varying stepsKeep the invariant flow in one placeTemplate Method or compositionInheritance can make variation rigid
The same business rule exists in several placesDo not repeat knowledgeExtract a focused policy or serviceA wrong abstraction can couple unrelated work
A class is merely long but changes for one coherent reasonPrefer clarity over class countingNo pattern yetAccept some length while keeping tests and names clear

This table is a starting point for discussion, not a pattern selection machine. A refactoring can use one of these ideas without reproducing every class in a textbook diagram.

11

Patterns should make tests smaller, not more ceremonial

A useful pattern creates a seam around meaningful behaviour. The notification policy can be tested without sending a real message. Each channel can be tested against the shared delivery expectations. The supplier adapter can be tested with representative responses and failure conditions.

Test the contract and the business result rather than asserting that a particular class name or call sequence exists. Otherwise the tests preserve the first implementation instead of protecting useful behaviour.

Integration tests still matter. An adapter that passes a unit test can still send the wrong field to a live supplier. Event based flows need checks for retries, duplicate handling and visible failures. A factory needs a test that proves each supported configuration creates the intended implementation.

If adding a pattern doubles the number of tests but does not make the important behaviour easier to verify, the abstraction may be serving the diagram rather than the software.

12

Watch for pattern fever

Design patterns give teams a useful vocabulary. They can also give ordinary code a costume party.

Warning signs include interfaces with one implementation and no credible alternative, factories that only call one constructor, events used to avoid an honest direct dependency, repositories that repeat every method from an existing data tool and a folder structure that requires five files to find one business rule.

The don't repeat yourself principle also needs restraint. Two pieces of code can look similar while representing different business knowledge. Combining them too early creates an abstraction that changes for unrelated reasons. Microsoft's guidance makes the point plainly: duplication is better than coupling to the wrong abstraction.

A useful rule is to tolerate small duplication until the reason for similarity is understood. Extract the shared idea when the second or third real variation reveals a stable shape. Refactor in small steps and keep the tests passing while the structure changes.

13

Review the design in the language of future changes

A code review should not ask only whether a recognised pattern has been implemented correctly. It should ask what future change the structure makes safer and what new cost the team is accepting.

For the notification example, a good outcome is easy to describe. A developer can add a provider without editing the case approval rule. A supplier outage leaves a visible delivery attempt. A customer preference changes the selected channel without changing how each channel works. The tests can check the rule without contacting an external service.

Those are observable properties. They are more valuable than being able to point at a UML diagram and name every box.

Patterns are shared design experience, not laws. Use the principle to understand the pressure, choose the smallest structure that contains it and remove the pattern if the problem disappears. Good design should make ordinary change calmer, not make ordinary code feel more important.

If an established application has become difficult to change, I can help review where responsibilities, integrations and business rules have become tangled, then turn the findings into a sensible order of work.

Useful questions

Before introducing a software design pattern, ask:

  • What repeated problem or likely variation are we responding to?
  • Which part of the behaviour should remain stable?
  • Which detail is changing for a different reason?
  • Would a small function or direct dependency still be clearer?
  • Does the proposed interface use the application's language rather than a supplier's vocabulary?
  • Can the important behaviour now be tested in a smaller scope?
  • Where will selection, construction and failure handling remain visible?
  • What extra classes, indirection or operating cost are we accepting?
  • Can another developer explain the pattern from the problem it solves?
  • What evidence would tell us the abstraction is wrong?
Explore Laravel and Vue development
Daniel Mills

Written by Daniel Mills

Business understanding and hands-on software delivery.

I help owners and teams improve the software they rely on, replace fragile processes and turn new ideas into practical systems people can actually use.