Software Design Principles

Last edit: Sep 16, 2023

This section explains some of the most important software design patterns and principles and shows how to implement them in a modular architecture.

Single responsibility

The single responsibility principle states that a software entity should only have a single purpose. This is one of the main pillars of modularity.
Single responsibility means smaller units, lower complexity, and fewer reasons to change the code in the future as the application becomes more and more complex.

Open-closed principle

This principle states that software entities should be open for extension but closed for modification.
This means that you should not have to modify your existing module to make it work with a use case in your application or to make it work together with another module.

The abstract permission module should work for any concrete permission type you need in your application or in your custom modules. Similarly, your abstract payment module should be able to handle any concrete payment method types you implement in your ecommerce solution.
This makes it possible to have distinct, isolated units that work together in a predictable way without the need for constant modifications/adjustments in existing modules.

We achieved this goal by introducing the new hook system in the Core Module.
A hook is a place in the module's code that allows other modules to "tap in" to the module's functionality and provide a different behavior or to react when something happens.
A hook is a means of executing custom code (function) either before, after, or instead of the existing code.

Back to the payment module example:
A payment module provides a standard way and an interface (a set of rules) for other modules to extend its functionality.
It's an abstract module that delegates the payment process to other modules and communicates with the app. This is the only main focus for the module (single responsibility).
If you want to add a new payment type, you can implement it as a custom payment type module using the payment module's provided hooks, but you don't need to change the payment module's code.

Liskov Substitution and the Interface Segregation Principle

The Liskov Substitution Principle says that objects should be replacable with instances of their subclasses without altering the behavior.
We can translate this principle to our use case as: Independent modules implementing the same interface should be interchangeable without changing the interface or the consumer module or application.

The interface segregation principle states that a software entity should never be forced to implement an interface that contains elements which it will never use.

Keeping these two principles in mind can help us keep our module ecosystem clean and healthy:
During the planning and system design phase it's important to think of our modules as independent, interchangeable units where modules are not forced to provide data or implement functionalities outside their scope.

A simple example for this case:
Our admin module provides a standard way for other modules to render their own admin UI.
The User module can show the list of users, the Theme Manager module renders the theme switcher and theme editor UI, etc.
But if we have a utility module that doesn't need its own admin page -- we shouldn't force the module to implement one.

Another example could be an ecommerce solution where you want to implement various shipping types and carriers as independent modules. Most shipping types will need different data from your app, but that doesn't mean that every shipping type module should implement every specific method or property.
It's possible that a shipping type will need its own contract/interface. In this case it's the "main" ecommerce module's responsibility to make this possible for the utility modules.

Loose coupling

Try to avoid hard dependencies between modules (or at least: try to keep the number of hard dependencies at a minimum). A module with a hard dependency on another module cannot function properly without the module it depends on.

Turn your hard dependencies to soft dependencies whenever it's possible: a module with a soft dependency on another module can function properly without the other module, even if it has a dependency on the other module.

Instead of including a partial directly from another module you can:

  • Check if the other module exists in the application by using the core module's exists helper first, so you can provide a fallback in case the other module is missing

{% function exists = 'modules/core/lib/queries/module/exists', type: 'module', name: 'other_module' %}

  • Don't expect the other module's schema to be present in the application (and don't create/modify records that you don't own).
    Instead you can use hooks to tap into the other module's functionality and provide your own schema for your own data types.


Don't Repeat Yourself (DRY).
If you repeat anything that has already been defined in code, refactor it so that it only ever has one representation in the codebase. If you stick to this principle, you will ensure that you will only ever need to change one implementation of a feature without worrying about needing to change any other part of the code.
The rule of thumb here is: Don't over-engineer it, but at least try to be DRY.


You should try to keep your software components simple and unnecessary complexity should be avoided.
The code must be easily readable and understandable by you and other developers.
Follow the principles of 'Keep It Simple, Stupid' (KISS).
Hard to read or obfuscated code is difficult to maintain and debug. Don't be too clever, write code to be read.

YAGNI (You ain't gonna need it)

This principle comes from Extreme Programming (XP). It states that you shouldn't add new features to your code until you need them.
Don't over-engineer your code, even if you expect some future changes or new features. Requirements can change over time and adding features that you don't need at the moment can increase the cost of build.
But keep in mind: This principle only applies to capabilities built into the software to support a presumptive feature, it does not apply to effort to make the software easier to modify.


We are always happy to help with any questions you may have.

contact us