The KISS, YAGNI, DRY and Law of Demeter principles teach us from experienced people how to solve common problems. In this post, we will discuss each of them in detail and understand how they can help us create sustainable and professional code.
The KISS, YAGNI, DRY and Law of Demeter principles, like other aspects of software engineering, are not rigid rules that must be followed at all costs. Instead, they help developers reflect and seek alternatives to create code with a more professional tone.
In this post, we will learn about the history of each of these four principles. In addition, we will look at practical examples that can be used to solve everyday problems in the system’s development environment.
Applying software development principles is a characteristic that indicates a developer’s professional maturity. It’s not just about knowing how to program or mastering a specific language, but about understanding how to write code that serves its purpose well, that grows sustainably and that can be maintained by other people or even yourself months or years later, without requiring much effort.
When we write code, our concern is usually that the logic meets the functional requirements and that the system works correctly. Furthermore, when executing a program in languages such as C#, the code written by the developer is converted to binary code (via IL and JIT compilation), so for the machine, no matter which philosophy you follow, in the end, everything will be transformed into binary.
What we need to keep in mind is that well-written code is intended to benefit other developers—after all, they are the ones who will read it, maintain it and evolve it in the future.
Principles are important because, through them, we learn to reflect on what we are creating. They were developed by people who have had a profound influence on the world of systems development, from its inception to the present day. These are people who have experienced many problems and decided to write these principles down and pass them on so that others could learn from them.
More than fixed rules, these principles provide ways to reason about problems. By learning them, programmers can develop a critical and refined mindset. They stop just “coding tasks” and start designing solutions with a very important purpose: creating code that is maintainable over time.
Below, we will look at four principles: KISS, YAGNI, DRY and the Law of Demeter. You can access the code discussed in the post in this GitHub repository: Development Principles.
KISS, an acronym for “Keep It Simple, Stupid,” emphasizes that simplicity should be the primary goal of design. The central idea is that simple systems tend to be easier to understand, maintain and fix, which means they are cheaper.
The term originated in the United States Navy in the 1960s and was attributed to engineer Kelly Johnson. He reportedly challenged his team to design a new jet so that it could be easily repaired on the battlefield, using only tools available on the battlefield. In this context, the use of the word “stupid” is not an insult, but a pragmatic reminder: the simpler the system, the more resilient it will be in real-world situations, especially when there are few resources to deal with failures.
To demonstrate the use of KISS, let’s imagine a hypothetical scenario, where you need to create a Minimum Viable Product (MVP) and one of the requirements is that the project has a web API that receives a location and returns a temperature. Later, this API will do more things, but for now its function should be to just receive a location and return the temperature. So if we ignore KISS we could have the following:
public class WeatherForecastController : ControllerBase
{
private readonly WeatherService _weatherService;
public WeatherForecastController(WeatherService weatherService)
{
_weatherService = weatherService;
}
[HttpGet("weatherforecast/{location}")]
public async Task<string> Get(string location)
{
return await _weatherService.GetWeatherForecastAsync(location);
}
}
public class WeatherService
{
private readonly IWeatherRepository _repository;
private readonly IExternalWeatherApi _api;
public WeatherService(IWeatherRepository repository, IExternalWeatherApi api)
{
_repository = repository;
_api = api;
}
public async Task<string> GetWeatherForecastAsync(string location)
{
if (string.IsNullOrWhiteSpace(location))
throw new ArgumentException("Location is required");
var cached = await _repository.GetForecastAsync(location);
if (cached != null)
return cached;
var external = await _api.FetchForecastAsync(location);
await _repository.SaveForecastAsync(location, external);
return external;
}
}
Although this code works well for enterprise systems, for a simple scenario, in this case an MVP, it is overly complex. We can identify several dependencies in it such as caching, data validation, communication with external services and data recording.
Remember, the premise was an API that would receive a location and return a temperature, but the code above implements much more than that, and for an MVP it doesn’t matter how this data is returned, nor if it is true.
In this case, to meet the requirements, we could just do the following:
app.MapGet("/weatherforecast", (string location) =>
{
return Results.Ok("Sunny with 25°C");
});
Note that the example above is much simpler and perfectly meets the requirements, without the need for multiple classes and dependencies (even the controller was eliminated). The code has only one endpoint that receives a location and returns the temperature—that is, it is as simple as possible.
💡 Use KISS whenever you need to implement something that only needs to be functional and that does not have complexities as a premise. Focus on the minimum necessary to meet the requirements and discard everything else.
YAGNI, an acronym for “You Ain’t Gonna Need It,” focuses on avoiding implementing features that aren’t needed yet (and may never be needed).
The term originated in Extreme Programming (XP) development practices, an agile methodology created by Kent Beck in the late 1990s. At that time, many software teams wasted time trying to predict everything the system might need in the future, which resulted in bloated code, unused features and lots of bugs.
YAGNI emerged as a direct and effective response to this over-anticipation: it encourages incremental and iterative delivery, guided by real needs.
To put YAGNI into practice, let’s imagine a scenario where the premise is that you create a Model class to represent a User entity that has an ID, name, email and password.
If you didn’t follow the principle, you would already anticipate that one day users might have social profiles, avatars and notification preferences. So you create all of this now:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
// It's not in the requirements, but one day you might need it ;D
public string? AvatarUrl { get; set; }
public SocialProfile? Social { get; set; }
public NotificationPreferences? Preferences { get; set; }
}
public class SocialProfile
{
public string? SocialName { get; set; }
public string? SocialEmail { get; set; }
}
public class NotificationPreferences
{
public bool EmailEnabled { get; set; }
public bool SmsEnabled { get; set; }
}
Here, the social profile and notification classes were implemented even though they were not included in the initial requirements.
Although it may seem simple, this addition has code that will not be used and can also bring future bugs, since ORMs such as Entity Framework Core treat classes as database entities and this can increase complexity when dealing with database migrations, for example.
To follow YAGNI, you just need to do the basics—just follow the requirements. In this case, we could do the following:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
💡Use YAGNI when you are in doubt about whether or not to bring forward an implementation. If something is not very relevant and, more importantly, is not in the requirements, it probably means that it should not be implemented now. An alternative is to create a backlog item that can, at the right time, be added to the project.
The DRY principle, an acronym for “Don’t Repeat Yourself,” was formally introduced in the classic book The Pragmatic Programmer, published in 1999 by authors Andy Hunt and Dave Thomas.
DRY advocates that duplicate information and logic should be extracted to make the code reusable. Applying this principle avoids inconsistencies, facilitates maintenance and reduces errors. Furthermore, DRY is not limited to just repeated code, but to any duplication of knowledge or logic in the system, including business rules, SQL, validation, configuration, etc.
To implement DRY, let’s consider a common example: handling email formatting. So, let’s assume we have a controller class that has two endpoints, one for registering an email and the second for signing. Both of these endpoints have something in common: checking for whitespace in the email and for the at character (@) in the provided address.
In a scenario that does not follow DRY, this would be done as follows:
[HttpPost("register")]
public IActionResult Register([FromBody] string email)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
return BadRequest("Invalid email");
var normalized = email.Trim().ToLower();
return Ok($"Registered: {normalized}");
}
[HttpPost("subscribe")]
public IActionResult Subscribe([FromBody] string email)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
return BadRequest("Invalid email");
var normalized = email.Trim().ToLower();
return Ok($"Subscribed: {normalized}");
}
Note that both endpoints have the same code—they receive an email, perform a validation and return an error or the result of the normalized email. This code is duplicated, in which case we can apply DRY.
public static class EmailHelper
{
public static bool Normalize(string? input, out string normalized)
{
normalized = string.Empty;
if (string.IsNullOrWhiteSpace(input) || !input.Contains("@"))
return false;
normalized = input.Trim().ToLower();
return true;
}
}
[HttpPost("register")]
public IActionResult RegisterDry([FromBody] string email)
{
if (!EmailHelper.Normalize(email, out var normalized))
return BadRequest("Invalid email");
return Ok($"Registered: {normalized}");
}
[HttpPost("subscribe")]
public IActionResult SubscribeDry([FromBody] string email)
{
if (!EmailHelper.Normalize(email, out var normalized))
return BadRequest("Invalid email");
return Ok($"Subscribed: {normalized}");
}
In this new version, we created a class and a static method to validate and normalize the email address. The method receives a text (input) that can be null and returns two results, a boolean value (true or false) indicating whether the normalization was successful or not and an output string (normalized) that will contain the processed email, if the input is valid.
Finally, we call this method in both endpoints, thus eliminating code duplication and also making the email normalization logic accessible to any other part of the code that needs this functionality.
💡Use DRY whenever you notice that there is something that can be reused by other parts of the system, always aiming to eliminate repetition. But don’t limit yourself to just code. Remember that DRY can be used anywhere there is unnecessary duplication.
The Law of Demeter, also known as the Principle of Least Knowledge, emphasizes that each software unit should have limited knowledge about other units with which it is closely related, or simply: talk only to your close friends.
In practical terms, the Law of Demeter determines that a method of an Object A should only call methods of:
The Law of Demeter originated in 1986 and was proposed by Ian Holland. At the time, Holland and his colleagues were working on a project called Demeter, whose main objective was to study ways to develop more adaptable software, using object-oriented programming to minimize coupling between system components through more modular methods.
Thus, the Law of Demeter was formulated as a guideline to reduce dependency between objects and promote greater encapsulation.
To put the Law of Demeter into practice, let’s consider a common example where we have a customer object that includes a street address. If we wanted to access the street address of a customer, ignoring the Law of Demeter, we could do the following:
public class Customer
{
public Address Address { get; set; }
public Customer(Address address)
{
Address = address;
}
}
public class Address
{
public Street Street { get; set; }
public Address(Street street)
{
Street = street;
}
}
public class Street
{
public string Name { get; set; }
public Street(string name)
{
Name = name;
}
}
[HttpPost("customer/street-name")]
public ActionResult<string> GetStreetNameViolation([FromBody] Customer customer)
{
if (customer?.Address?.Street == null)
return NotFound("Street not found");
// Directly navigating through object graph (violation)
string streetName = customer.Address.Street.Name;
return Ok(streetName);
}
The code above has an endpoint that receives a customer and returns the street name of that client’s address.
The problem is that to get the street name, it navigates between the objects that are part of the customer, and this is exactly where the Law of Demeter is violated. Remember that an object should only access objects close to it? In this sense, we need to find a way to shorten the path and reduce the dependency between the controller endpoint and the street name.
So, we can do the following:
// New version of the Customer class
public class Customer
{
public Address Address { get; set; }
public Customer(Address address)
{
Address = address;
}
// Method that follows the Law of Demeter
public string GetStreetName()
{
return Address?.Street?.Name;
}
}
// New version of the Controller that follows the Law of Demeter
[HttpPost("customer/street-name")]
public ActionResult<string> GetStreetName([FromBody] Customer customer)
{
var streetName = customer?.GetStreetName();
if (string.IsNullOrEmpty(streetName))
return NotFound("Street not found");
return Ok(streetName);
}
Note that the Customer class has received a new method: GetStreetName()
, which accesses properties and objects until it finds the street name. Furthermore, the new controller only accesses the customer object. This means that the controller only interacts with the Customer, which is its direct collaborator. The Customer is the one who knows how to access Street.Name
. The navigation logic is encapsulated within the object itself, and this is in harmony with the Law of Demeter.
Even though Customer.GetStreetName()
internally navigates by Address?.Street?.Name
, this is contained within the class itself, which has full right to know its internal structure.
💡Use the Law of Demeter whenever you want or need to create loosely coupled code. It is not a hard and fast rule, but rather a design guideline to promote encapsulation and loose coupling between objects.
Creating systems is complex and full of subtleties, and it is inevitable that problems will arise during this process. The good news is that many people have already gone through these problems and decided to help others overcome them. That is why software design principles exist—they help us create more sustainable systems that are less prone to errors.
In this post, we covered four principles: KISS, YAGNI, DRY and the Law of Demeter. We also saw how to practically implement each of them.
I hope this post has helped you understand what each of these principles means, and, more importantly, that they help you create incredible things by reflecting on what they teach us.