Design Principles in Software Development Part 1
Table of Contents
- Introduction
- DRY Design Principle
- SoC Design Principle
- DIE Design Principle
- KISS Design Principle
- YAGNI Design Principle
- TDA Design Principle
- Conclusion
Introduction
This blog covers some of the most widely used design principles in software development. These principles help developers write code that is maintainable, testable, and scalable. Whether you are an experienced developer or just starting out, understanding these principles will improve the quality of your code.
The following design principles are covered in this post:
- DRY — Don't Repeat Yourself
- SoC — Separation of Concern
- DIE — Duplicate Is Evil
- KISS — Keep It Simple, Stupid
- YAGNI — You Ain't Gonna Need It
- TDA — Tell Don't Ask
DRY Design Principle
DRY stands for Don't Repeat Yourself. This principle states that you should avoid repeating the same code in multiple places. If you find functions or code being used in multiple locations, it is better to move them to a common place. Otherwise, you will have to maintain the same code in two different spots.
This principle applies to any area of software development — whether you are structuring front-end SCSS files, creating JavaScript functions, or developing back-end SQL queries. Below is an example using Node.js with TypeScript.
Example: Extracting Shared Constants
Imagine you are developing Page A and create a class that needs a readonly string for a blog detail URL, so you define it in Page A. Later, you notice that Page B also needs the same URL. Based on the DRY principle, you should move it to a common location that both classes can access — perhaps a constants file.
// page A
private readonly BLOG_DETAIL: string = "/blog/post/";
// page B
private readonly BLOG_DETAIL: string = "/blog/post/";
Example: SCSS Variables
The DRY principle also applies when creating SCSS files. If you declare a variable for your primary color in one SCSS file and that variable is needed in other SCSS files, it is better to declare it in a global variables file that all SCSS files can import.
Example: Avoiding Duplicate Functions
I have seen cases where a developer creates one file for localStorage operations in JavaScript and another nearly identical file for sessionStorage operations. The only difference is localStorage versus sessionStorage, but the code ends up duplicated. This is a missed opportunity to apply the DRY principle.
The DRY principle may seem simple, but consistently keeping it in mind can lead to significant improvements. For instance, how do you share the same UI component across two different systems — one for a public-facing UI and another for an internal UI? Approaches like Micro Front-End architecture can help solve this type of problem while staying true to the DRY principle.
SoC Design Principle
SoC stands for Separation of Concern. This principle is about dividing a software program into distinct sections or units, where each section has its own purpose. Following this principle leads to better maintainability, easier code refactoring, and improved testability.
Imagine if one class had to handle everything — it would be very difficult to understand, maintain, and debug.
Example: Express.js Route
Let's look at the following code. This Express.js route takes a query string key, uses that value to query MongoDB for localization data, and then binds the data to a view called local-view. The code is not too long, follows the DRY principle, and appears to work. So what's wrong?
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let local = new Schema({
key: {
type: String
},
value: {
type: String
},
culture: {
type: String
}
});
async getLocalText(key) {
if (!key) {
return '';
}
try {
return await localModel.findOne({key: key});
} catch (error) {
console.error(error);
return '';
}
}
exports.all = async function(req, res) {
const model = await getLocalText(req.params.key);
res.render('local-view', model);
}
This code can be broken down into the following sections, each with its own responsibility:
- Database Model — Defines the MongoDB schema
- Database Repository — Repository function to access the MongoDB database
- View Model — Model to bind to the view
- Controller — Checks the request query, gets the model, and binds it to the view
Why Separation Matters
What happens if you don't separate concerns? Your code becomes a "God class" — a class that does everything. God classes are hard to maintain, hard to add features to, hard to debug, and hard to test.
By creating a separate file for the database model, you can add validation and define the schema in one place that can be shared with other repositories. By creating a separate database repository file, that file only needs to handle database access and connections. The same goes for the view model and controller — each has its own focused purpose.
DIE Design Principle
DIE stands for Duplicate Is Evil. This principle is similar to DRY but emphasizes that duplication is inherently harmful to code quality. When writing new code, you should actively work to eliminate duplicates.
However, there may be cases where you choose not to refactor duplicates, especially when other factors take priority.
When to Refactor Duplicates
If you join a team and notice many duplicate constants or functions, should you change them? The answer depends on several factors:
- Your error rate — How likely are you to introduce bugs during refactoring?
- QA automation — Do you have a trusted automated test suite to catch regressions?
- Team trust — Does your team trust you to make refactoring changes safely?
If you are new to a company and immediately send a PR to refactor everything, it may go well if nothing breaks. But if you accidentally break something, it could take time to rebuild trust. Timing and context matter when applying this principle.
KISS Design Principle
KISS stands for Keep It Simple, Stupid. This principle means keeping your software design as simple as possible and avoiding unnecessary complexity. While this sounds straightforward, writing simple code often takes more effort than writing complex code. You need to review your code repeatedly to find simpler approaches, reduce unnecessary logic, and test again.
Example: Two Approaches to Bug Fixing
Consider a scenario where you need to fix a bug that seems complex. Developer A goes in and fixes the issue but creates several new functions in the process. Developer B fixes the same issue with just one line of code. Which approach is better?
Developer B likely spent more time understanding the root cause of the issue before writing any code. This brings us to a few practices that help maintain simplicity:
Tips for Keeping It Simple
-
Find the real problem first — Invest time in understanding the issue before writing code. The reason Developer B needed only one line might be that they spent more time understanding the problem.
-
Keep files small — If a file is growing large, it likely contains mixed responsibilities. Breaking it into smaller, focused pieces helps maintain simplicity.
-
Do code reviews — Collaborate with your coworkers to review and simplify code. Pair programming and code reviews are great ways to write simpler, cleaner code.
YAGNI Design Principle
YAGNI stands for You Ain't Gonna Need It. This principle is related to KISS and advises against adding functionality until it is actually needed. When designing software, keep it simple and avoid implementing features based on speculation about future requirements. If you don't need it now, don't build it now.
Adding unnecessary features increases complexity, creates more code to maintain, and can lead to bugs in features that may never be used. Focus on the current requirements and add new functionality only when there is a clear need for it.
TDA Design Principle
TDA stands for Tell, Don't Ask. This principle suggests that an object should be responsible for its own data and behavior, rather than requiring the caller to query the object's state and perform calculations externally.
Example: Before Applying TDA
In the following example, the caller creates a LineItem object and manually calculates the total:
class LineItem {
ItemPrice: number;
Quantity: number;
Total: number;
}
const myItem = new LineItem();
myItem.ItemPrice = 30;
myItem.Quantity = 2;
myItem.Total = 2 * 30;
Example: After Applying TDA
Now, we refactor the code so the LineItem class calculates the total internally. The caller simply tells the object the price and quantity, and the object provides the total:
class LineItem {
constructor(itemPrice: number, quantity: number) {}
get Total(): number {
return this.itemPrice * this.quantity;
}
}
const myItem = new LineItem(30, 2);
console.info(myItem.Total);
As a LineItem class grows with more logic and properties, it might make sense to separate its business logic and data properties into different files, continuing to apply other principles like SoC.
Conclusion
Design principles are essential tools for writing high-quality software. Here is a summary of the six principles covered in this post:
| Principle | Full Name | Key Takeaway |
|---|---|---|
| DRY | Don't Repeat Yourself | Move shared code to a common location |
| SoC | Separation of Concern | Each module should have one focused responsibility |
| DIE | Duplicate Is Evil | Actively eliminate code duplication |
| KISS | Keep It Simple, Stupid | Write the simplest code that solves the problem |
| YAGNI | You Ain't Gonna Need It | Don't build features you don't need yet |
| TDA | Tell, Don't Ask | Objects should manage their own data and behavior |
These principles are guidelines, not rigid rules. Apply them thoughtfully based on your project's context, and they will help you write more maintainable, testable, and scalable code.