RSS Amplifier

Architecture Corner · Jan 20, 2026

A Quick Explanation of SOLID Principles

0
Sign in to vote or save

Architecture Corner · Architecture Corner

Software development has its fair share of best practices, principles, patterns, and rules. SOLID is one of them that is misunderstood and taken to the extreme, generating the usual opposing stances: love them or hate them.

If you have ever had trouble understanding them or what their benefits are, let’s inspect them, one by one.

Often presented as “each class should have a single responsibility,” this principle is all about ensuring the class has only one reason to change.

For example, if your class calculates the quote for an insurance and also has the code responsible for saving it, then if either one changes, you would need to change the class, violating the principle.

Instead, we could have

Why is it important: isolating these parts that have distinct purposes can help your code to be easier to understand, test, and maintain.

Tips:

  • I am usually ok when a first iteration still has these separate pieces of code together. You are usually unsure of the boundaries, and as long as you refactor, things tend to be ok

  • Be careful not to split your code to the point where it becomes harder to understand what is happening

This may seem contradictory at first, but the idea is that you should be able to extend the class behavior without modifying it through abstractions.

Imagine your class handles credit card payments through third-party services. You can extend your class by supporting new third-party services.

If you need to add another processor service, it would mean changing the class even if the external behavior is the same.

Tips:

  • Capture the intent you want in an interface (or base class, depending on the case) and make that part of the dependencies of your class/function.

Even if the first two principles take some time to master, they are very straightforward to understand. This one is not as direct:

“If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.”

Maybe it is just me, but this definition, while correct, is somewhat abstract and could benefit from some refinement.

So, we already know we can use interfaces/base classes without OCP. Liskov substitution is achieved if the behavioral contract is the same* among all derivatives.

That violates the principle, as one of the derivatives, if used, would fail because it has a different precondition.

Tips:

  • Your derivatives should have the same or weaker preconditions

  • Your derivatives should have the same or stronger postconditions

Back to an easy one :)

When creating your abstractions, favour smaller, more focused interfaces to avoid classes having to implement behaviors that they do not support.

Instead, you could break it down into smaller ones

Tips:

  • I may be easier to think about the role a given class will have to determine the size of the interface

  • There is nothing wrong with an interface that has a single behavior, but avoid the extreme of having too many interfaces if all their behavior always comes from the same type of derivative

The idea here is that you should depend on abstractions (contracts) instead of concrete implementations. So instead of having your application code with a PostgreSQL dependency you could say it depends on some form of persistence.

What type of persistence? Well, at the application level, you do not care to the point where it could be replaced without affecting the rest of the code.

Those who refute this claim with the argument that no one replaces the persistence. And they are partially right ;-) It is rare, and when it happen,s the changes are so drastic that having the lower level dependency in your application is the least of your concerns.

However, we would be missing the point.

If we correctly apply the dependency principle, we free ourselves from making some decisions upfront and help keep the code focused, going back to the SRP.

The efforts to make LLMs more powerful and useful continue. Back in October 2025, Claude introduced the concept of Skills, which is a way to tell how the LLM should handle specific tasks. Since then, it has been published as an open standard for cross-platform portability.

In it, you define instructions in the form of .md files, scripts, and resources that will be loaded as needed.

OpenAI has added initial support for skills.md on both ChatGPT and Code CLI, which, like MCP, is another standard that Claude has contributed, and now it’s making its way to most IDE/CLI agents.

Expect a dedicated article on skills in the near future!

Microservices have been around for a while, and even when properly understood and applied, require us to dedicate attention to the testing, which changes from a traditional monolithic approach.

ThoughtWorks released an article a while ago dedicated to the topic, showcasing the many types of tests and what they should cover when you choose the microservice way.

We start by looking at the testing strategies:

  • Unit

  • Integration

  • Component

  • Contract

  • End-to-End

And then the implications on the traditional pyramid as a way to balance the cost of developing and maintaining the tests versus the benefits they provide.

If we consider the above the logical + physical boundaries of your service, then it becomes easier to understand what each strategy should accomplish.

For example, at the unit level you may want to treat the unit under test as a black box to observe changes in the state (sociable unit testing) and/or the interactions between it and the dependencies (solitary unit testing) via doubles.

So, from the proposed breakdown, this could mean resources, service layers, repositories, and gateways would get solitary, while the domain could have sociable unit testing. Lower-level elements, like HTTP Clients or libraries - ORM - would be excused.

When doing integration testing, we would be focusing on testing the communication paths and the interactions between components and their external dependencies. The goal would be to detect interface defects.

As the article goes on, it explores other complementary strategies that you should be aware of and decide if they apply to your solution and to what extent.

For example, the component testing would isolate the external dependencies and just focus on the inner interactions between the elements that are part of the component. If you had an API, it could look like invoking the UI layer (HTTP), but without requiring the request to come from the outside, making sure the expectations are met.

Contract testing is next, and it really is important when you have a more complex public API and many different clients that use it in a slightly different way. By having them contribute to defining tests on how they interact with your service, you may catch regressions that escaped your suite.

The article covers the end-to-end tests and the challenges associated with them, including the data preparation that needs to enable the use cases and to focus on the personas that will use your system as a whole.

Because developing and maintaining those tests takes time and resources, the traditional pyramid should be considered, where the base contains the highest number and the cheapest to execute tests.

Out of the tests described, contract testing is usually the least common, while end-to-end are the hardest to automate given its scope and number of dependencies.

Git is the “de facto” source version control used for most projects, be them public or private.

Its branching capabilities offer a lightweight way to diverge from the main line of development and work on new capabilities simultaneously. It can isolate multiple paths, or branches, and is really powerful for teams.

But what if you want to take this to an “extreme”? Imagine you want to develop, for experimentation purposes, with different versions of the same feature and compare those implementations side by side.

Since you can only have one active branch at a time, when you check out or switch to a different branch, your directory automatically moves to the new branch. The solution at this point would be to have two different directories, each with a separate branch, which can be time-consuming for big repositories and awkward for your IDE.

Worktress enables you to have multiple branches from the same repository available, each in its own directory, without the need to switch or perform a stash.

My first reaction to this feature was nice, but not something that affected me or most of the workflows/projects I am involved with. That was until now :)

With AI assistants growing in capabilities, I am facing more and more the following: asking the agent to perform a certain task and keep waiting while it is done, before asking to do another one in the same repository.

Or I really want to experiment with two different approaches (or even models) to solve the same issue.

Now, certain tools, like Visual Studio Code + Copilot, enable local background executions. So it means I can delegate work to multiple local agents.

It would start the work and allow me to continue using the IDE normally. For example, I want to test two versions, one using DynamoDB as well.

While I continue my research or execution.

When they finish, I will have both immediately available for inspection, comparison, and additional tweaks.

All of this thanks to worktrees. You can delete the worktrees you do not need anymore or merge them, and deal with conflicts in the case they touch the same files as you normally would.

While this workflow may not be useful for all cases, it can help you get most of those potentially parallel but related tasks.

No posts

Read the original on architecturecorner.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.