#177 - Simple Object-Oriented Design: Principles for Writing Clean & Maintainable Software - Mauricio Aniche
“Every software gets more complex over time. What we need to do as engineers is to find ways so that we can work with increasing complexity, but not increasing the cost of maintaining the software."
Mauricio Aniche returns to the podcast for the second time and discuss with me his latest book, “Simple Object-Oriented Design”. Our discussion explores the intricacies of software design and shares practical strategies to manage software complexity through effective object-oriented design.
Mauricio delves into the six key principles of a simple object-oriented design: making code small, keeping objects consistent, managing dependencies, designing good abstractions, handling external dependencies, and achieving modularisation.
This episode is a must-listen for anyone seeking to deepen their understanding of object-oriented design and maintaining simplicity in their codebase!
Listen out for:
- Simple Object-Oriented Design Book - [00:03:19]
- No Perfect Code Design - [00:04:51]
- Managing Complexity - [00:06:37]
- Object-Oriented Design - [00:08:24]
- Design as an Everyday Activity - [00:09:43]
- Effective Iterative Design - [00:12:31]
- Refactoring - [00:14:31]
- 6 Principles of a Simple Object-Oriented Design - [00:16:40]
- Principle #1: Making Code Small - [00:21:06]
- Arguments Against Smaller Units - [00:23:18]
- Principle #2: Keeping Objects Consistent - [00:26:25]
- Don’t Fight Your Framework - [00:30:03]
- Principle #3: Managing Dependencies - [00:32:05]
- Separate High-Level (What) and Low-Level Code (How) - [00:33:34]
- Principle #4: Designing Good Abstractions - [00:36:31]
- Finding the Balance in Abstraction - [00:38:39]
- Principle #5: Handling External Dependencies and Infrastructure - [00:41:05]
- Principle #6: Achieving Modularisation - [00:45:12]
- Owing to Junior Developers - [00:49:18]
- 3 Tech Lead Wisdom - [00:50:32]
_____
Mauricio Aniche’s Bio
Dr. Maurício Aniche’s life mission is to help software engineers to become better and more productive. Maurício is a Tech Lead at Adyen, where he heads the Tech Academy team and leads different engineering enablement initiatives. He is the author of the “Effective Software Testing: A Developer’s Guide” and “Simple Object-Oriented Design” published by Manning.
Maurício previously held a position as an assistant professor of software engineering at Delft University of Technology in the Netherlands, where his teaching efforts in software testing gave him the Computer Science Teacher of the Year 2021 award and the TU Delft Education Fellowship, a prestigious fellowship given to innovative lecturers.
Follow Mauricio:
- Website – mauricioaniche.com
- LinkedIn – linkedin.com/in/mauricioaniche
- Twitter / X – @mauricioaniche
Mentions & Links:
- 📚 Simple Object-Oriented Design – https://www.manning.com/books/simple-object-oriented-design
- 📚 Philosophy of Software Design – https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201
- 🎧 #139 - A Developer’s Guide to Effective Software Testing – https://techleadjournal.dev/episodes/139/
- Domain-driven design – https://en.wikipedia.org/wiki/Domain-driven_design
- Hexagonal pattern – https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)
- Ports and adapters – https://8thlight.com/insights/a-color-coded-guide-to-ports-and-adapters
- John Ousterhout – https://en.wikipedia.org/wiki/John_Ousterhout
Check out FREE coding software options and special offers on jetbrains.com/store/#discounts.
Make it happen. With code.
Get a 40% discount for Tech Lead Journal listeners by using the code techlead24 for all products in all formats.
Tech Lead Journal now offers you some swags that you can purchase online. These swags are printed on-demand based on your preference, and will be delivered safely to you all over the world where shipping is available.
Check out all the cool swags available by visiting techleadjournal.dev/shop. And don't forget to brag yourself once you receive any of those swags.
Simple Object-Oriented Design Book
-
One of the premises that I had there was that I wanted a book that doesn’t strive for the perfect design. Because it feels like to me, sometimes I read a book about object-oriented design and the design is perfect. And that assumes that the person knows a lot about the problem already. And that’s not what happens in real life.
-
You get a very messy requirement that you don’t know so much about it, that is going to change. And you cannot go for the best design ever. So you have to take pragmatic decisions so that you can move forward and deliver software.
No Perfect Code Design
-
When I learned object-oriented design more than 20 years ago, I thought this was just beautiful. They could create objects and they could talk to each other. And then if you really do your best, then those objects, they just look so pretty and so elegant. But in real life, life is way more messy than the examples you’ve seen in books and things change in real life.
-
In my 20 years of experience, it was never possible to achieve perfection. We always had to at some point settle for something. And we should be okay with that because that’s just life. We don’t want to take the most awful decision possible. Because that’s really bad, that creates technical debt. But you also don’t have to strive for perfection. Something in between is good enough.
Managing Complexity
-
Software becomes complex just because life gets more complex. We’re sort of trying to implement software to solve people’s problems, but those problems get more complex. And if you don’t pay attention, your code will also get complex, and that’s natural.
-
It’s expected that every software gets more complex over time. And I think what we need to do as engineers is to find ways so that we can keep increasing complexity, but not increasing the cost of maintaining this software. Sometimes people try to say that you should find a way so that software never grows in complexity. I don’t believe that is really possible. I think it’s indeed the other way around. You have to just live with the fact that it will get more complex.
Object-Oriented Design
-
All those paradigms are good enough and you can build good software with all of them. It’s a functional programming, data-oriented programming that is getting more traction right now. I feel it’s just about picking the paradigm that works best for you.
-
If you start a project today using object-oriented programming, it is a good choice. You will be able to write maintainable software for sure.
Design as an Everyday Activity
-
For me, one of the greatest powers in object-oriented programming is that you can create abstractions. And then abstractions, they reduce the complexity, because suddenly you don’t have to handle specific implementation details, you just focus on the abstraction. Those abstractions are more extensible. They provide usually extension points, so on and so forth. So object-oriented programming can be very powerful.
-
When you have the design in mind, you’re like, oh, okay, it feels like those I have to implement those 10 different business rules and 10 if statements are the easiest possible way for me to do this. But is this a good design? Is this going to help me in maintaining this in the future, in testing this, in evolving this?
-
And if you make this into a day-to-day activity, it hurts you less. And you mentioned you’re designing something and then you have corner cases etc and those things really hurt your abstractions.
-
That’s the real life I want to bring into this book. That you create this beautiful abstraction, it seems like it works, you deploy, and then suddenly you find a corner case that your abstraction doesn’t fit. And that’s just normal. But then, once you’ve come back, you have to have your design mindset in mind. And do I need to improve my design or if I just had this if statement here, is this like a huge sin that I’m making right now?
-
So as long as you have, you’re always thinking of the design, I think you’re fine. If you’re not thinking of design and just coding, then I think that you’re not really using the power of object-oriented programming.
Effective Iterative Design
-
Effective design comes up after multiple iterations.
-
He does mention that in his experience, he only gets the perfect “design” after the third time he’s working on that problem. So you try once, then you learn something. You try the second time, you learn more. The third one looks a little bit better.
-
And when I read that paragraph in the book, I was like, yes, that makes a lot of sense. I feel like for all the things that I did something really beautiful and elegant and flexible, it was because I already knew the problem for so long. And that was the realization for me that you will never do something in your first shot that really looks elegant in the way you want.
-
That then leads me to the argument of my book. That is, if you know that’s just what’s going to happen, you are not going to come up with something elegant from the first try, you have to keep monitoring yourself. And as soon as you learn a little bit more, then you have to invest in refactoring this and improving it. Because if you do this at the very beginning, odds are that it’s cheaper than doing this 5 years later where the codebase is much bigger, everything is much bigger.
-
Putting refactoring on the loop and not being ashamed of changing a design decision that I took like three months ago, but it is already a wrong decision. Maybe it’s cheaper than if you do it later.
Refactoring
-
Tooling is important, knowledge is important, but also culture. Because I’ve been to teams where refactoring was just part of the culture and people would really appreciate when you’re doing this type of commits in the code base.
-
But I’ve also been to teams where this was seen as a bad thing. Like you’re not being productive, you’re not really delivering business rules, new functionality, etc. So I think culture is also a big part of how you make it possible.
-
If you’re in a culture where refactoring is well-seen, then lucky you. Just do it. If you’re not, then maybe you should first change the culture rather than trying to introduce new tools or knowledge sharing sessions, because they will not be enough.
6 Principles of a Simple Object-Oriented Design
-
The first one in the book is making the code small. Think of lines of code even. A code that has less lines of code is much easier to maintain than a method that is super large. One of the arguments I made there is that you have to make sure that your units are always small. It’s better to have multiple small units than one super large unit.
-
Then I also talk about keeping objects consistent. So if you’re in an object-oriented system, your objects hold data, and those objects commonly relate to other objects and they together represent some sort of very complex business functionality and it’s very common that you change something inside of the object and you have to propagate this change and make everything consistent.
- If you don’t pay attention, it’s very easy for you to end up in a design where things are inconsistent. This is of course very problematic, leads to bugs. So you’ve got to make sure that they are consistent at all times.
-
Then I talk about managing dependencies. So in object-oriented systems, you depend on things from, for example, one class depends on another class or a module depends on another module. And then there I discuss a few rules on how you can manage those dependencies because you cannot avoid dependencies. You have to depend on smaller classes to make the whole behavior work. But how do you make sure that those dependencies don’t hurt you so much?
-
I then talk about abstractions and how to design good abstractions. And to me, this chapter is key because this is key in object-oriented programming. Making sure that in parts of the system that really need a lot of flexibility, making sure that you have a good abstraction there. So it’s easy to keep adding more business rules or making sure that you can easily change something that happens in a very complex flow is very important.
- If you’re doing this very complex system that has to calculate taxes for 100 different countries, you don’t want to have 100 if statements in one single big class. And you want it to be easy to add country number 101, because apparently that’s part of your business.
-
Then I talk a lot about how to handle external dependencies and infrastructure. At some point, your system will have to talk to some external system, let’s say, a database. You have to persist stuff into the database. Or you have to make a web service call to an external party. Or you have to use a library that you didn’t write yourself.
- So it is very common that in these systems you have to integrate with things you don’t really own and control. And if you don’t pay attention, it’s very easy to get super coupled to this external infrastructure. Or some of your code is hard to test because you depend on this third party, so on and so forth.
-
And finally, I talk about modularization. In very large software systems, you have multiple domains within one design. And then, of course, you don’t want to have this in one big messy bucket. You want to have different modules, and those modules talk to each other. So this is almost like seeing the coupling between classes that I spoke at the beginning of the book, but now we’re talking about modules. Modules talking to other modules. This chapter makes a lot of sense if you’re really working on a very large software system.
Principle #1: Making Code Small
-
For me, a rule of thumb, number one there, and the most important one is to try to keep your units very small. And by units, I mean your methods, try to keep them small. And your classes, try to keep your classes as small. And if they start to grow, and they will start to grow at some point, refactoring number one is how can I move some code around?
-
Maybe your public method starts to grow and then you do that extract method refactoring that everyone sort of knows. Your IDE does for you. You move to a different method.
-
If you’re in a small unit, you are going to understand it much, much, much faster. And when you move a piece of code to another class, you’re basically saying, I don’t have to read this piece of code when I’m reading this unit that I’m focusing on right now.
-
You’re going to give this class a name. You’re going to give the method a beautiful name. So when I’m reading the original class and I see a method call to there, that method call explains to me what is going on. I don’t have to read the implementation details of that.
-
Once you’re forced to only work in small units, you’re forced to write code that explains intent first and then implementation later. And finding ways not to read implementation is what makes you more productive. You don’t wanna read every detail.
Arguments Against Smaller Units
-
Your first argument is a common argument that usually people that defend having everything in one place raise. That is, I don’t want to navigate through different classes and methods. And the point is, if you’re really designing with abstraction in mind, you won’t have to jump into those other classes as much as you think you would.
-
Because usually you know what you’re doing is you read the intent, and then you find out this is the specific part I need to really dive into, and then you just dive into that specific part. If your design is forcing you to read a thousand lines of code every time you have to do some maintenance on that piece of code, the abstraction is wrong.
-
Navigating isn’t so much of a problem today anymore. IDEs are also very good. So, even if sometimes you have to do some navigation, just learn how to use your IDE and that will make you way more productive.
-
The other comment from you was the naming. Yes, indeed, naming is very tricky. I don’t even attempt to give a lot of tips on how to name things because I feel like the best one is to keep trying, keep refactoring as soon as you learn more about the domain.
-
Just keep trying, just keep refactoring. The more you learn about the domain, the more you rename it. But once you get to a reasonably good name, it doesn’t have to be a perfect name, but a reasonably good name, then life’s much better. Because you just read the method call and then you know what it does without going to that method.
-
And then sometimes you’re like, where do I break this code? Maybe you don’t find natural spots for you to extract something to another method or to another class. That is the hard part in real life. Because sometimes there’s no natural cutting point for you to extract something. But you have to keep working until it emerges and you find them.
Principle #2: Keeping Objects Consistent
-
By consistency, I mean, whenever you have an object, you have to trust on the state of that object.
-
The answer should be correct. You shouldn’t have to do a lot of work yourself to make sure that the object is in the correct state it needs to be. The object needs to do it by itself. But it’s easier said than done.
-
Because as soon as objects start to grow and then they start to depend on other things, and then suddenly you have this very complex aggregation of objects representing some very complex business rule. Then it gets really tricky because maybe one object super down the line changes something and this change needs to be propagated upstream and downstream.
-
This is when the problem starts to happen in these very complex entities, at least in my experience. To me, the discussion around this is where to ensure the consistency. Who should ensure the consistency?
-
The first argument that I make is it should never be the client. So the client of that class should never be the one ensuring consistency. It should be somewhere else. But then, in the book, I go into the details, because sometimes, to ensure consistency, you also need to make a database call. Because maybe you need some data from the database.
-
And then, usually we don’t like to make database calls from entities. Because we don’t like to couple entities with databases. So then this means I cannot really do the consistency check inside of my entity. So this starts to accumulate.
-
In the book, I give some rules of thumb. For example, the first place you should try is the entity itself. Maybe inside of the class, if all the information is there, just do it there. So you don’t need to go somewhere else. But then when I need a database call or whatever, then maybe I have to introduce something in between that ensures the object is always in a consistent state. This is less ideal, of course, because then you have to make sure that clients don’t make the call to the entity without going through this small layer you had to create. So you have to play a little bit with your design to make sure that problem doesn’t happen.
-
The point is, you got to ensure your objects are consistent. And you have to find a place to make it consistent, and you have to make it simple for the client. It should not be the responsibility of the client to ensure that the object is consistent.
Don’t Fight Your Framework
-
Frameworks are opinionated. And they do this because they want to give you more productivity. If you just follow their rules, things are much better for you.
-
The suggestion I make in my book is don’t fight your framework. As soon as you pick your framework, accept its decisions, and see what you can do so that they don’t really harm you that much. I feel like modern frameworks nowadays, they are quite less opinionated than they were in the past, so that makes your life a little bit easier. You have more freedom in designing. But some frameworks from the past, indeed, they were quite opinionated.
-
Why do I say this explicitly in the book? Because I see sometimes advice on how you can abstract your whole domain model away from your framework or for whatever other decisions you take. So it really lives in a bubble.
-
Maybe for some very specific cases, you really want to abstract everything away from your domain. But in most software systems that I’ve seen, it is okay to be coupled to the framework. You’re not going to move from frameworks every month or so. So I think it’s also about finding the right balance between, I want to be productive and I want to have a perfect design.
Principle #3: Managing Dependencies
-
If I have a class that depends on the other 20 classes, everything becomes more complex, right? The code is just more complex because it probably makes calls to those 20 dependencies. Those dependencies do stuff, they change state in the system and then you have to handle all these things. So this just increases complexity. And of course, the natural problem of coupling that is if one dependency changes and it depends on that, maybe if it’s a breaking change that will also impact you. So I think the less dependencies, the better as a general rule.
-
Of course, you have to always depend on stuff. And then the whole point of the exercise is how to find this balance. For example, maybe instead of depending on 10 classes, maybe you can group some of those dependencies together. So you get three of those dependencies, and then you group them into another class. You give a beautiful domain related name to this class that groups these three other dependencies, and the original class now only depends on one instead of depending on the other three.
-
More often than not, when you look at those classes, you can find groups, things that you can group. And give a new domain term for this operation and move this to another place. So this is usually refactoring number one that I feel quite feasible for a lot of the business systems out there.
Separate High-Level (What) and Low-Level Code (How)
-
To me, nice code is the one that when you read it, you read the intent, and not implementation details.
-
This becomes very important once you’re in a very complex business rule that is not only complex from an intent point of view, but also from an implementation point of view. And there, what you want is you want to open the first class that implements that business rule. And you want to read like a 10, 15, 20 lines that look like an algorithm. It only expresses what happens, not how it happens.
-
And this is, to me, the separation between high level and low level. You don’t want this for your whole system. But for the complex parts, what you want to do is to have this entry point that is purely intent. And then you let low level classes implement the intent and do the job and do the hard work.
-
This is the separation that I discuss a lot in my book. And it’s very important, because it does help you to manage dependencies. When your dependency is focused on intent, it’s more high level.
-
Think of an interface. Imagine you have some business rule and you have 10 implementation of this business rule. So we give it a common interface. This interface expresses intent, it’s high level, and it’s very stable. So if you’re depending on such an interface, you have a dependency there, but this dependency doesn’t hurt you so much because interfaces like this tend to be very stable.
-
What you have is for this part of the code base, you want to have a code that expresses intent and only depends on high-level things like interfaces and abstract classes. And this tends to be quite stable. And then lots of low-level classes that implement those interfaces and get the job done.
Principle #4: Designing Good Abstractions
-
That’s the one million dollar question. I think finding the perfect abstraction is impossible. What you want to find is something in between that solves the problem.
-
To me, it always comes from trying to understand what will evolve, what I believe is going to evolve in my system.
-
We want an abstraction that truly represents the intent of the system because the intent of that part of the system will evolve. And you do this by thinking of how can I give extension points? How can I facilitate the extension of this design for the future? I don’t want the person to come back and write if statement number 20 to implement a business rule. I want to simplify this a little bit.
Finding the Balance in Abstraction
-
The best there would be to have a crystal ball, right? So we could predict, precisely, what will change and what will not change. And we make mistakes on both sides. So we sometimes over design and we create something more complex for something that doesn’t have to be complex. And other times we under-design. So we create something too simplistic.
-
Of course, experience is something that helps as time goes by. You learn better to listen to your requirements. And then understand if you need an abstraction there or not.
-
The other one is, you try to observe. So you start very simple. Don’t overcomplicate from the beginning unless you have a clear reason for it. And then as you go, then you add an abstraction.
-
I feel like we said so much, don’t over design, don’t over design, don’t over design. That I feel the main problem right now is that we are under designing things more than over designing things.
-
Don’t be afraid of sometimes doing over design because if you feel like this is going to need an extension point in the future, go for it. Don’t be afraid of that.
Principle #5: Handling External Dependencies and Infrastructure
-
No systems live in the vacuum. At some point, you’re going to have to integrate this with another system, being it a database or web service and so and so forth. And so you have to find ways to connect your beautiful object-oriented design to an external system that speaks a different language, that has a different abstraction than you have.
-
If you don’t think so much about it, and let’s say you just code everything in one big class, suddenly you’re very dependent on that abstraction, on that system. If that system changes, it’s going to propagate a lot of changes to you, harder to test, so and so forth. So you have to explicitly think about external systems, external dependencies, and how you handle them.
-
I give a few tips in the book. And the first one is, I find it quite simple actually to do, is separate infrastructure from domain codes. Whenever you’re going to have to talk to a third party, let’s say a database, just don’t do this in the same place you do business rules. Just create another class whose only responsibility is to translate your domain entity into a database call, let’s say. Move it to somewhere else. Don’t let them stay together. That will already improve a lot. If there’s a change in your database, you just have to change one point. You just go to your database class and you just change there. Your entities and your domain are more testable because it’s easier for you to replace that small part of the system that is a database with a mock, for example, so only advantages.
-
Sometimes you read books, people on the internet saying you should hide the external infrastructure as much as possible from everywhere else. Like your code should not know you’re using a relational database, your code should not know that you’re using a NoSQL database or whatever.
-
And I feel like this is such a bad advice because if you really want to write something that is fully agnostic of the many technologies you have to choose to deliver software, you’re going to have to write so much code. And it’s way more complex and way more likely to have a bug and probably doesn’t use your infrastructure to the best, because if you create something agnostic, you are a sort of leveling down everything else.
-
I understand where this comes from. Because back to my first point here, you don’t want to make your code really coupled to the external party. But there’s a limit there. And usually my argument in the book that I make is what you need to do is you have to hide those decisions from the code, but not from the developer that is maintaining that code.
-
So the developer knows it’s a relational database behind the scenes. So you can use all the advantages that relational databases give to you. But if you look at the code, the code of course knows it’s a relational database, but a lot of it is a little bit abstracted away.
-
You have to separate these things, but you’re not pretending you don’t know them. You know them and it’s good that you know them because you can take optimal decisions.
Principle #6: Achieving Modularisation
-
Sometimes your different domains grow very much. Maybe you even want different teams working on them because you’re that big right now. How do you make these modules to communicate with each other? Because although they are isolated, they still depend on each other.
-
This is to me the moment where you need modularization. I copied the term deep modules from Philosophy of Software Design because I really loved that term. But I think I rephrased it a little bit to my own point of view.
-
Something we want to for sure combat is that idea that’s everything needs to be a module. You don’t want to have 1 million modules or 1 million microservices, because whenever you split stuff, you win something, but you also lose something. So you want to find the right trade-off there.
-
Deep modules is basically the idea that what you want is, a module needs to do something very “big” it has to have like a beefy responsibility. So it is not just one class, one class should not be a module in most cases. So it does something really beefy, really important for the business, and it also offers a very simple layer that simplifies all the complexity that is inside of that module.
-
This is the type of module you want to have in your system. Complex stuff that is exposed to the outside in the simplest way possible. So that the clients, others that want to communicate with your module, they have to do the least amount of effort to be able to send you a message.
-
This is where, to be honest, where the challenge starts. Because it’s creating a simple interface on top of something very complex is tricky.
-
There are ideas like come up with sensible defaults. You don’t have to force your clients to have to configure every single possible variable that you have in there. Simplify things, and this is where you need design.
Owing to Junior Developers
-
Whenever you’re coding and you’re trying to be pragmatic, remember that at some point there will be a junior developer that will maintain this. So it is our duty to make sure they can learn from the code we’re writing.
-
When I was a junior developer, I was learning a lot from what I was reading. So you owe this to the junior developer you’re directly or indirectly training.
3 Tech Lead Wisdom
-
Focus on your technical debt, because at some point it can become impossible to pay. Everything that we discussed about refactoring early, etc, I truly believe it makes a difference in practice.
-
Focus on quality. Don’t let it be a second-class citizen in the process. Find ways to make sure quality is just part of the process, part of the culture.
-
Something that is a lot on my mind right now, given the team that I’m working with, is focusing on developer experience. Because coding should be fun and fast, not boring and slow. But once you grow, it is very hard to keep things that way. So they have to put some energy there.
[00:01:04] Introduction
Henry Suryawirawan: Hello, everyone. Welcome back to another new episode of the Tech Lead Journal podcast. Today, I have a repeat guest. So Mauricio Aniche. He was in episode 139, so almost one year ago. Today, he’s back with his new book “Simple Object-Oriented Design”. So today we’ll talk about software complexity, object-oriented programming, and what kind of a design that Mauricio is advocating so that we can maintain the complexity of our software and make sure that the design performs well and we can always maintain the software. So welcome back Mauricio to our show. So looking forward for this conversation.
Mauricio Aniche: Thanks, Henry. It’s a pleasure to be back.
Henry Suryawirawan: Right. Mauricio, so it’s been almost a year since we last chat with each other. So maybe if you can give us a glimpse what you were up to in the past one year.
Mauricio Aniche: For sure. So for those that didn’t listen to the previous one, my name is Mauricio. I work in the software industry for 20 years. I’ve been a developer, I’ve been a full-time researcher, now I’m back to industry. And in the past two and a half years, I worked for Adyen. Adyen is a financial technology company. And right now, my current duties are to lead the team that we call testing enablement team. We do basically tools that help developers to create tests. We do code quality related tools and this type of stuff. So yeah, that’s indeed what I’m up to.
Yeah. So I think for those of you who haven’t listened to the previous episode by Mauricio, right, we talk a lot about testing. Effective testing, in fact. So there are a lot of insights I learned a lot from that episode. So make sure to check it out as well if you haven’t listened to that episode.
[00:03:19] Simple Object-Oriented Design Book
Henry Suryawirawan: So today we’ll be talking about your new book, Simple Object-Oriented Design. So maybe in the beginning, explain to us like why you came up with this book. It seems like a little bit different from the first book that you wrote a few years ago.
Mauricio Aniche: For sure. And actually, I just got a copy here. So if you’re watching the video, you can see the beautiful cover that Manning has prepared for this one. And I think the motivation for this book was that one of my best friends, Alberto, him and I, we always talk about code design, right? How to design classes so that you can implement your business focused system in a nice way. And Alberto loves design and we talk a lot about it. And all those conversations, they triggered me and I felt, maybe I should just, you know, try to write down the best practices I observed throughout my career. And hopefully, in a very short and concise way. So this book is quite short, I think 150 pages is the final printed version. So that’s how this book came to be.
And I think one of the premises that I had there was that I wanted a book that doesn’t strive for the perfect design, right? Because it feels like to me, sometimes I read a book about object-oriented design and the design is perfect. And that assumes that the person knows a lot about the problem already. And that’s not what happens in real life, right? You get a very messy requirement that you don’t know so much about it, that is going to change. And you cannot go for the best design ever. So you have to take pragmatic decisions so that you can move forward and deliver software. So that’s what I wanted to try to summarize in this book.
[00:04:51] No Perfect Code Design Henry Suryawirawan: So you mentioned that, I mean, your career 20 years, right? So you open the book by saying that after 20 years being in the industry, previously you try to achieve perfection, you know, maybe perfect code design. But you currently don’t believe that it is true anymore, right? So you think that probably there’s no perfect software out there. So tell us your revelation about this and maybe what we can learn from that revelation as well.
Mauricio Aniche: Yeah, for sure. I think for me it was when I learned object-oriented design more than 20 years ago, I thought this was just beautiful, you know, they could create objects and they could talk to each other. And then if you really do your best then those objects, they just look so pretty and so elegant. But in real life, life is way more messy than the examples you’ve seen in books and things change in real life and etc. So I feel like it’s… and I think in my 20 years of experience, although right now I don’t write business logic, right, so I’m in platform engineering so I’m more writing tools for developers. But in my previous experiences, I was always, you know, a back end developer implementing business rules. I guess, like a lot of the people out there. It was never possible to achieve perfection, right? We always had to, you know, at some point settle for something. And we should be okay with that because that’s just life, right? So we don’t want to take the most awful decision possible, right? Because that’s really bad, that creates technical debt. But you also don’t have to strive for perfection. Something in between is good enough.
Henry Suryawirawan: Yeah, so I think over in my career as well, especially when we work with multiple people in the teams, right, with various degree of experience. So I think it’s really hard to maintain quality across everyone, right? So sometimes we have to let go some parts of it. But as long as the, maybe, general high level design is good, we can always maintain and maybe refactor from time to time, right?
[00:06:37] Managing Complexity
Henry Suryawirawan: Which brings me to the next discussion about software complexity. So I think it’s very funny. I mean, we all have been there. We try to build a new system, right, from scratch. We think that this is going to be different this time, right? So we’ll build a perfect software. But I think over the time software gets complex and, again, we come to the same cycle, that software is becoming quite complicated to change. So why software can become complex over the time and what we should do in order to maintain that?
Mauricio Aniche: Yeah, for sure. So software becomes complex just because life gets more complex. Right, so we’re sort of trying to implement software to solve people’s problems, but those problems get more complex. And if you don’t pay attention, your code will also get complex, and that’s natural. I think it’s expected that every software gets more complex over time. And I think what we need to do as engineers is to find ways so that we can keep increasing complexity, but not increasing the cost of maintaining this software. Sometimes people try to say that you should, you know, find a way so that software never grows in complexity. I don’t believe that that is really possible. I think it’s indeed the other way around, you have to just live with the fact that it will get more complex.
Henry Suryawirawan: Yeah, especially as you keep on building on top of what’s existing, right? So I think more business logic, maybe new features, right? Sometimes edge cases that you didn’t think before and somehow it gets introduced, but it just doesn’t fit rightly, right? So in order to make it quick, so you just add as quickly as possible as well.
So I think, I do agree, especially for business related software, right? I think it tends to get complicated. Especially these days, people also overcomplicate the architecture, right? So maybe using microservices or maybe different databases and things like that. I think it makes the software get more complicated as well.
[00:08:24] Object-Oriented Design
Henry Suryawirawan: So I think another thing that I am interested in your book, you cover object-oriented design. I think these days, many people kind of like have different flavor of programming languages. Object-oriented design and functional programming probably are the two biggest. So maybe your view, is object-oriented still relevant or should people think about some kind of different paradigms?
Mauricio Aniche: I think all those paradigms are good enough and you can build good software with all of them, right? It’s a functional programming, data-oriented programming that is getting more traction right now. I feel it’s just about picking the paradigm that works best for you. And for me, object-oriented programming was always the one that my mind could understand better. So I don’t think it’s a bad decision. I think if you start a project today using object-oriented programming, it is a good choice. You will be able to write maintainable software for sure.
Henry Suryawirawan: Yeah. So I think you mentioned as well in your book that object-oriented helps you to build maintainable software. Of course, if done right. But I think, yeah, it’s also a very nice abstraction in a way, right? So you can kind of like think in objects, right? So some people find it more intuitive. I do find it more intuitive as well, but I think different kind of problems also sometimes you can choose different programming paradigms which may be more suitable. For example, maybe data pipeline kind of thing is more suitable for functional programming.
[00:09:43] Design as an Everyday Activity
Henry Suryawirawan: So yeah, let’s probably cover the object-oriented part this time. You mentioned that we always have to keep design as part of our activity in software development. So I think this is probably intuitive in a sense, but not practiced a lot by different people because they assume that the design is there. I just need to add on top of what exists, right? So tell us how can we do more design activities in our software development?
Mauricio Aniche: For me one of the greatest powers in object-oriented programming is that you can create abstractions. And then abstractions, they reduce the complexity because suddenly you don’t have to handle specific implementation details, you just focus on the abstraction. Those abstractions are more extensible. They provide usually extension points, so on and so forth. So object-oriented programming can be very powerful. If you’re always thinking of how to make sure I’m building the right abstractions. And when I say you have to always be developing with this designing lines, it is because if you don’t think of abstractions and you know, how should my classes look like, what should be my extension point so on and so forth. You know, you’re just going to go to the code and you’re going to just write the implementation that solves the problem. Then let’s say maybe this is, you know, 10 ifs in a row that solves the problem for sure, but is this maintainable? And when you have the design in mind, you’re like, oh, okay, it feels like those I have to implement those 10 different business rules and 10 if statements are the easiest possible way for me to do this. But is this good design? Is this going to help me in maintaining this in the future, in testing this, in evolving this?
And if you make this into a day to day activity, it hurts you less. And you mentioned, when you’re talking about my previous comment that, you know you’re designing something and then you have corner cases and etc and those things really hurt your abstractions, right? That’s the real life I want to bring into this book, right? That you create this beautiful abstraction, it seems like it works, you deploy, and then suddenly you find a corner case that your abstraction doesn’t fit. And that’s just normal. But then once you’ve come back, you have to, you know, have your design mindset in mind. And do I need to improve my design or if I just had this if statement here, is this like a huge sin that I’m making right now? So as long as you have, you’re always thinking of the design, I think you’re fine. If you’re not thinking of design and just coding, then I think that you’re not really using the power of object-oriented programming.
Henry Suryawirawan: So I think that’s really, really insightful, right? So think about abstractions. And always try to look back whether the existing abstraction or design actually fits the problem as it evolves. And I think you also quoted multiple times in the book, right, John Ousterhout. And one of the quote is actually talking about effective design actually comes up after multiple iterations, right? After maybe one or two or three times, you work on the same problem, right? And somehow you find the right design afterwards.
[00:12:31] Effective Iterative Design
Henry Suryawirawan: So I think what I can see in practice in the real world is that sometimes we come up with first design, have few problems along the way, but we didn’t actually refactor or redesign. And we just build on top of it until it gets so complicated that we probably think of, you know, we should rewrite this. So tell us how can we actually have more dare to actually do a lot of redesigns throughout all the software development?
Mauricio Aniche: Indeed, so that book influenced me a lot. It’s a book that I read much later in my career, because it’s more of a recent book, right? But it has so many good gems that I really loved. And although I don’t agree 100 percent with that book, right, I think I even write this in my book that there are some parts that I don’t fully agree. But he does mention that in his experience, he only gets the perfect “design” after the third time he’s working on that problem, right? So you try once, then you learn something, you try the second time, you learn more, the third one looks a little bit better. And when I read that paragraph in the book, I was like, yes, that makes a lot of sense. I feel like for all the things that I did something really beautiful and elegant and flexible, it was because I already knew the problem for so long. And I had tried already my first POC. And you know, that was the realization for me that you will never do something in your first shot that really looks elegant in the way you want.
That then leads me to the argument of my book, that is, if you know that’s just what’s going to happen, you are not going to come up with something elegant from the first try, you have to keep monitoring yourself. And as soon as you learn a little bit more, then you have to invest in refactoring this and improving it. Because if you do this at the very beginning, odds are that it’s cheaper than doing this 5 years later where the codebase is much bigger, everything is much bigger. So I feel like putting refactoring on the loop and not being ashamed of, you know, let me change a design decision that I took feels like three months ago, but it is already a wrong decision. Maybe it’s cheaper than if you do it later. So that’s sort of the argument I make there.
[00:14:31] Refactoring
Henry Suryawirawan: Right. So I think refactoring is always something that it takes discipline, right? So from every developer to actually have the, you know, optimism to continuously doing it. Also having effective testing. So it’s very important because when you refactor, you don’t want to break previous features or have some regression bugs. And I think importantly as well, like use different kind of tools to help you in the refactoring. Maybe use the modern IDEs, right? Or maybe use AI these days that can help you explain the code and maybe do kind of like a local optimization. So all these things should help you to support your refactoring activity and hopefully the good design can come up from there.
Mauricio Aniche: Yes, I was going to say tooling is important, knowledge is important, but also culture. Because I’ve been to teams where refactoring was just part of the culture and people would really appreciate when you’re doing this type of commits in the code base, right? But I’ve also been to teams where this was seen as a, you know, as a bad thing. You know, like you’re not being productive, you’re not really delivering business rules, new functionality, and etc. So I think culture is also a big part of how you make it possible. If you’re in a culture where refactoring is well-seen, then lucky you. Just do it, right? If you’re not, then maybe you should first change the culture rather than trying to introduce new tools or knowledge sharing sessions because they will not be enough.
Henry Suryawirawan: Yeah, I think that’s a very good pointer, right? So culture is very important indeed. So sometimes developers may be afraid to say, I am refactoring this design because it wasn’t right. So people may think, okay, why didn’t you do it right in the first time? So I think a good culture, especially the people around you who support refactoring as well, I think that makes a big difference. And probably I would add as well a coach that probably can help you point out like which parts that should be refactored. I think that also helps as well.
Mauricio Aniche: And also why doing this at the beginning makes more sense, right? Because I can understand if a developer blocks a merge request that does a refactoring. Five years later where the code is way more sensitive, etc. It is much harder to block such merge requests if it’s at the beginning when the code is still fresh, you know, just out of the oven, if you will. So that’s why it needs to be constant.
[00:16:40] 6 Principles of a Simple Object-Oriented Design
Henry Suryawirawan: Yep. So let’s move on to your six principles of how we can make a simple object-oriented design, right? So I think the keyword here is simple. You mentioned it a couple of times as well, simple. Sometimes it’s not intuitive as well what is simple, right? So maybe if you can walk us through the six characteristics or six principles that you outline in the book at a high level. And afterwards maybe we can go slightly deeper one by one each in the next conversation, yeah.
Mauricio Aniche: For sure. The first one in the book is making code small. And the intuition there is that code that is small, so think of lines of code even. A code that has less lines of code is much easier to maintain than a method that is super large, right? So one of the arguments I made there is that you have to make sure that your units are always small. It’s better to have multiple small units than one super large unit.
Then I also talk about keeping objects consistent. So if you’re in an object-oriented system, your objects hold data, and those objects commonly relate to other objects and they together represent some sort of very complex business functionality and it’s very common that you change something inside of the object and you have to propagate this change and make everything consistent, right? So if you have, I don’t know, a shopping cart and a list of products and items, if you add a new item, you need to make sure that the cart has the new total price to pay, for example. This is what I mean by consistency. And if you don’t pay attention, it’s very easy for you to end up in a design where things are inconsistent. So maybe the list of items in your cart, if you sum all the prices of them, the sum is different from the total value you have in your shopping cart object. And this is of course very problematic, leads to bugs. So you’ve got to make sure that they are consistent at all times.
Then I talk about managing dependencies. So in object-oriented systems, you depend on things from, for example, one class depends on another class or a module depends on another module. And then there I discuss a few rules on how you can manage those dependencies because you cannot avoid dependencies. You have to depend on smaller classes to make the whole behavior work. But how do you make sure that those dependencies don’t hurt you so much?
I then talk about abstractions and how to design good abstractions. And to me this chapter is key because this is key in object-oriented programming. Making sure that in parts of the system that really need a lot of flexibility, making sure that you have a good abstraction there. So it’s easy to keep adding more business rules or making sure that you can easily change something that happens in a very complex flow, is very important, right? So the example is you don’t want to, you know, if you’re doing this very complex system that has to calculate taxes for 100 different countries, you don’t want to have 100 if statements in one single big class. And you want it to be easy to add country number 101, because apparently that’s part of your business, right? So this type of stuff is very important if you’re looking for simple object-oriented design.
Then I talk a lot about how to handle external dependencies and infrastructure. And what I mean by that is when you’re designing your object-oriented system, you are thinking of classes and how they relate to each other and how they keep consistency among each other and blah, blah, blah. And this is all beautiful and isolated, right? This lives in this design world, if you will. But at some point, your system will have to talk to some external system, let’s say a database, right? You have to persist stuff into the database. Or you have to make a web service call to an external party. Or you have to use a library that you didn’t write yourself. So it is very common that in these systems you have to integrate with things you don’t really own and control. And if you don’t pay attention, it’s very easy to, you know, get super coupled to this external infrastructure. Or some of your code is hard to test because you depend on this third party, so on and so forth.
And finally, I talk about modularization. That is, in very large software systems, you sort of have multiple domains within one design. And then, of course, you don’t want to have this in one big messy bucket. You want to have different modules, and those modules talk to each other. So this is almost like seeing, you know, the coupling between classes that I spoke at the beginning of the book, but now we’re talking about modules. Modules talking to other modules. This chapter makes a lot of sense if you’re really working on a very large software system. So those are the six principles that I cover in the book.
[00:21:06] Principle #1: Making Code Small
Henry Suryawirawan: Yeah, so I think indeed those are the six principles, six characteristics that you think will lead us to a simple and better object-oriented design, right? So I think let’s probably try to cover each of them in more details. So the first one is something about making code small. So you mentioned about, you know, maybe lines of code, maybe smaller classes or functions. So is that the only thing that we should care about in terms of making code small, or are there any other practices that we should think about in terms of trying to make our code small?
Mauricio Aniche: Yeah. So for me,a rule of thumb, number one there, and the most important one is try to keep your units very small. And by units, I mean your methods, try to keep them small. And your classes, try to keep your classes as small. And if they start to grow and they will start to grow at some point, refactoring number one is how can I move some code around? So that it leaves this big class and then it goes to another class or to another method, right? Maybe your public method starts to grow and then you do that extract method refactoring that everyone sort of knows. Your IDE does for you. You move to a different method. So that’s sort of the gist of what I discussed in that chapter.
And my point there being that if you’re in a small unit, you are going to understand it much, much, much faster. And when you move a piece of code to another class, you’re basically saying, I don’t have to read this piece of code when I’m reading this unit that I’m focusing on right now. And if you give, let’s say you extract a bunch of lines to a new class. You’re going to give this class a name. You’re going to give the method a beautiful name. So when I’m reading the original class and I see a method call to there, that method call explains to me what is going on. I don’t have to read the implementation details of that. So I feel like, once you’re forced to only work in small units, you’re forced to write code that explains intent first and then implementation later. And finding ways not to read implementation is what makes you more productive. You don’t wanna read every detail, right? Once you’re, you know, maintaining something you just care first about the intent and then you go crazy and debug whatever is going on. So that’s sort of my argument number one in the book that I think it’s not so hard to follow and I think it does make a lot of difference
[00:23:18] Arguments Against Smaller Units
Henry Suryawirawan: Yeah, so I think intuitively it’s not so hard to follow. But I think, again, in practice, it seems very hard to have the discipline to actually continue doing this, right? So one of the arguments, probably, some people might have these arguments. So jumping into multiple methods or classes breaks the flow. You have to open multiple tabs and, you know, navigate through different files. And the second thing is about finding good names. Yeah, of course, we have to be able to explain the intent, but sometimes finding a name or function name or method name or class name is not so easy. So maybe if you have tips for these two things.
Mauricio Aniche: For sure. And your first argument is a common argument that usually people that defend having everything in one place usually raise, right? That is, I don’t want to navigate through different classes and methods and etc. And the point is, if you’re really designing with abstraction in mind, you won’t have to jump into those other classes as much as you think you would. Because usually you know what you’re doing is you read the intent and then you find out this is the specific part I need to really dive into, and then you just dive into that specific part. If your design is forcing you to read a thousand lines of code every time you have to do some maintenance in that piece of code, the abstraction is wrong. So, I think that’s usually my response to that. Navigating isn’t so much of a problem today anymore, right? IDEs are also very good. So, even if sometimes you have to do some navigation, just learn how to use your IDE and that will make you way more productive, for sure.
And then the other comment from you was the naming. Yes, indeed, naming is very tricky. I don’t even attempt to give a lot of tips on how to name things because I feel like the best one is keep trying, keep refactoring as soon as you learn more about the domain. I think that’s actually the rule, like the suggestion I give in the book. You know, just keep trying, just keep refactoring. The more you learn about the domain, the more you rename it. But once you get to a reasonable good name, it doesn’t have to be a perfect name, but a reasonably good name, then life’s much better, right? Because, again, you just read the method call and then you know what it does without going to that method.
And also, maybe, maybe a comment you said, it’s very hard. It is. Because, you know, life is hard. And then sometimes you’re like, where do I break this code, right? Maybe you don’t find natural spots for you to extract something to another method or to another class. In my book, I give an example, right? Sometimes you try to extract something to a private method, but then you have to take together, you know, you create a function with 10 parameters. And you’re like, yeah, do I want a function with 10 parameters? You know that looks very weird to me and blah, blah, blah. So that is the hard part in real life. Because sometimes there’s no natural cutting point for you to extract something. But you have to, you know, keep working until it emerges and you find them.
Henry Suryawirawan: Yeah. So I think thanks for the tips, right? So for those of you who still find it difficult to extract the units to become smaller and smaller, please do give it a try. I see some developers also try to create sections within the big function by putting in comments. So it’s like giving names to a certain sections, right? But it’s still one big line. So instead of doing that, maybe extract as a private method and give it a proper name that express the intent. So I think those are really, really great tips, right? So please try to make your units smaller.
[00:26:25] Principle #2: Keeping Objects Consistent
Henry Suryawirawan: So let’s move on to the next principle, which I find this is also tricky, sometimes, in object-oriented. You mentioned about keeping objects consistent. I think in some theory, they call it invariance as well, right? So when you create objects, right, it should maintain the invariance within that objects. So tell us more about this inconsistency, right? Why it is a problem, why it tends to create a lot of bugs?
Mauricio Aniche: Yes, sure. So by consistency, I mean whenever you have an object, you have to trust on the state of that object, right? So if you have a shopping cart in your hands, if you call a method “get me the total amount of this cart”, “give me how many products are there”, you have to trust the answer. The answer should be correct. You shouldn’t have to, do a lot of work yourself to make sure that the object is in the correct state it needs to be. The object needs to do it by itself. But it’s easier said than done, of course, because as soon as objects start to grow and then they start to depend on other things, and then suddenly you have this very complex aggregation of objects representing some very complex business rule or whatever. Then it gets really tricky because, you know, maybe one object super down the line changes something and this change needs to be propagated upstream and downstream, whatever.
And I think that’s what you need to make sure, this is when the problem starts to happen, right, in this very complex entities, at least in my experience. And in the book, I tried to make a discussion, because to me the discussion around this is where to ensure the consistency. Who should ensure the consistency? Should it be, so for example, the first argument that I make is it should never be the client, right? So the client of that class should never be the one ensuring consistency, it should be somewhere else. But then, in the book I go into the details, because sometimes, you know, to ensure consistency, you also need to make a database call. Because maybe you need some data from the database. And then, usually we don’t like to make database calls from entities, right? Because we don’t like to couple entities with databases. So then this means I cannot really do the consistency check inside of my entity. So this starts to accumulate.
And then in the book, I give some rules of thumb. For example, first place you should try is the entity itself. Maybe inside of the class, if all the information is there, just do it there. So you don’t need to go somewhere else. Ah, but then I need a database call or whatever, then maybe I have to introduce something in between that ensures that that object is always in a consistent state. This is less ideal, of course, because then, you know, you have to make sure that clients don’t, you know, they make the call to the entity without going through this small layer you had to create. So you have to play a little bit with your design to make sure that this happens or this doesn’t happen, that that problem doesn’t happen. But this is the trick. And maybe I went too technical right away, but then if I zoom back a little bit, I think the point is you got to ensure your objects are consistent. And you have to find a place to make it consistent, and you have to make it simple for the client. It should not be the responsibility of the client to ensure that the object is consistent.
Henry Suryawirawan: I think that’s indeed a very important point, right? You have to ensure that the client does not understand how to assemble your consistent object, right? So to speak, right? So I think sometimes this tends to happen if let’s say you are a single person trying to work on those multiple classes, right? Because maybe the understanding leaks all over the places. So you unconsciously create that behavior simply because you were not so conscious about the design, right? And I think sometimes also frameworks kind of like lead us to that thing, right? So for example, in some frameworks you have the getter setters that are exposed as if like you can call that publicly. So I think I can see some legacy code last time, where you create a new object, right? And then you have set one, set two, set three, set four. I think that’s also like a best practice.
[00:30:03] Don’t Fight Your Framework
Henry Suryawirawan: And I think you mentioned a few times about entity and maybe aggregates, right? So I think using domain-driven design as the concept to maintain a good design is something that is very, very useful for us to implement. So, if let’s say, those people who are stuck with their framework, what can you suggest in order for them to be conscious about it, that’s the first thing, and also try to adopt a better design?
Mauricio Aniche: Yeah, good question, because frameworks are opinionated, right? And they do this because they want to give you more productivity. If you just follow their rules, things are much better for you. And the suggestion I make in my book is don’t fight your framework. As soon as you pick your framework, accept its decisions, and see what you can do so that they don’t really harm you that much. I feel like modern frameworks nowadays, they are quite less opinionated as they were in the past, so that makes your life a little bit easier. You have more freedom in designing. But some frameworks from the past, indeed, they were quite opinionated. I remember suffering a lot with JSF, you know, and this type of technologies from the days. So don’t fight your framework.
And why do I say this explicitly in the book? Because I see sometimes advice on how you can, you know, abstract your whole domain model away from your framework or for whatever other decisions you take. So, you know, it really lives in a bubble. I feel like great, you know, maybe for some very specific cases, you really want to abstract everything away from your domain. But in most software systems that I’ve seen, it is okay to be coupled to the framework. You’re not going to move from frameworks every month or so. So I think it’s also about finding the right balance between, I want to be productive and I want to have a perfect design, you know.
Henry Suryawirawan: Yeah, so indeed, right, sometimes framework gives you a boost of productivity. But also do understand the best practice, right, in terms of design and what we should avoid in terms of how to make, for example, the classes, function calls and things like that. So be aware of that. And I think one thing very important to make sure object is consistent is about validation, right? So having the precondition or maybe some kind of validation rules before you create an object or execute something is very, very important.
[00:32:05] Principle #3: Managing Dependencies
Henry Suryawirawan: Maybe let’s move on to the next one, which is about managing dependencies. I think the very, very first advice you give is minimize dependencies. The less, the better. So how can we minimize dependencies?
Mauricio Aniche: Yeah. So, why do I think this is a good advice? Because if I have a class that depends on other 20 classes, everything becomes more complex, right? The code is just more complex because it probably makes calls to those 20 dependencies. Those dependencies do stuff, they change state in the system and then you have to handle all these things. So this just increases complexity. And of course, the natural problem of coupling that is if one dependency changes and it depend on that, maybe if it’s a breaking change that will also impact you. So I think the less dependencies, the better as a general rule.
But you, of course, you have to always depend on stuff. And then the whole point of the exercise is how to find this balance. And for example, maybe instead of depending on 10 classes, maybe you can group some of those dependencies together. So you get three of those dependencies, and then you group them into another class. You give a beautiful domain related name to this class that groups these three other dependencies, and the original class now only depends on one instead of depending on the other three. So very more often than not, when you look at those classes, you can find groups, things that you can group. And give a new domain term for this operation and move this to another place. So this is usually refactoring number one that I feel quite feasible for a lot of the business systems out there.
[00:33:34] Separate High-Level (What) and Low-Level Code (How)
Henry Suryawirawan: Yeah. So I think that’s a very, very useful tips, right? So if you start seeing a lot of, maybe imports, right? So in a lot of programming language, you can sense dependencies by looking at the import statements, right? If you do see a lot of classes that you imported or dependencies you import, so maybe that’s the first sign, right? That you maybe should try breaking them up into multiple units, right? And I think another useful tips that I find in your book is you mentioned separate high level and low level code, right? So separating the what and the how. So tell us about this principle and how we can implement this?
Mauricio Aniche: Sure. So I mentioned before in this interview, right, that I think to me, nice code is the one that you, when you read it, you read the intent, and not implementation details. And this becomes very important once you’re in a very complex business rule that is not only complex from an intent point of view, but also from an implementation point of view. And there, what you want is you want to open the first class that implements that business rule. And you want to read like a 10, 15, 20 lines that look like an algorithm. It only expresses what happens, not how it happens. And this is, to me, the separation between high level and low level, right?And you don’t want this for your whole system, right? Because you don’t need this for your whole system. But for the complex parts, what you want to do is to have this entry point that is purely intent. And then you let low level classes implement the intent and do the job and do the hard work, right?
So this is the separation that I discuss a lot in my book. And I think it’s very important, because it does help you to manage dependencies, right? When your dependency is focused on intent, so it’s more high level. Think of an interface, right? So imagine you have some business rule and you have 10 implementation of this business rule. So we give it a common interface. This interface expresses intent, it’s high level, and it’s very stable. So if you’re depending on such an interface, you have a dependency there but this dependency doesn’t hurt you so much because interfaces like this tend to be very stable. So what you have is for this part of the code base, you want to have code that expresses intent and only depend on high level things like interfaces and abstract classes. And this tends to be quite stable. And then lots of low level classes that implement those interfaces and get the job done.
So this is funny because this section of this book, I was always in doubt, should I fit this into managing dependencies or design good abstractions? And in the original, one of the first drafts of the book, this was one chapter, but it became too big that I decided to break. But those two chapters, this one and the one we’re going to discuss next, they have a huge intersection.
Henry Suryawirawan: So I think that’s a very useful tips. Again, from my understanding, maybe if I summarize it, so you separate the high level intent from the low level implementation, right? So if the client always refers to the high level intent, they don’t actually need to see all these low level dependencies. And hence you actually manage dependencies by minimizing those classes that they need to import or they need to understand, right? So I think that’s a very useful tip for a better design.
[00:36:31] Principle #4: Designing Good Abstractions
Henry Suryawirawan: The next one you mentioned about good abstractions, right? I think, this is sometimes tricky. So finding good abstractions is always difficult. Unless you kind of like have a well defined business rules that stays the same. But if it’s like a novel software, I think it’s really, really difficult to find good abstractions. How can we find a better abstractions, Mauricio?
Mauricio Aniche: Yeah, that’s the one million dollar question. I think finding the perfect abstraction, as of being, you know, restating here is impossible. What you want to find is something in between that solves the problem. And I think that that comes, to me it always comes from trying to understand what will evolve, what I believe is going to evolve in my system.
And maybe a concrete example. Many years ago, I worked for this company and I was part of the billing team. So we would do everything related to how we charge the customers up to later, calculating the taxes we had to pay in, etc. And it was so complex, right, because the company was offering a lot of services. There were lots of different ways to charge the customer, but also to pay for taxes later on. So we sort of understood that, you know, charging in different ways was something that had to be core of this. And also calculating different ways of taxes was also something very core to this business. So we spent a lot of time trying to think of how can we create an abstraction to represent billing and an abstraction to represent taxes. And that was a huge exercise of us talking to the business people understanding the different types of taxes, refining the abstractions until we got to the point that whenever there was a new way of charging someone, we would just, you know, implement a bunch of interfaces and voila. This would plug naturally into the system and the same for taxes.
So this is, I think that that was sort of the example I had in mind when I wrote this chapter about good abstractions, right? So we want an abstraction that’s truly represents the intent of the system because, you know, the intent of that part of the system, because, you know, it will evolve. And you do this by thinking of how can I give extension points? How can I facilitate the extension of this design for the future, right? I don’t want the person to come back and write if statement number 20 to implement a business rule. I want to simplify this a little bit. Does that make sense to you?
[00:38:39] Finding the Balance in Abstraction
Henry Suryawirawan: Yeah, yeah. I mean, in a sense, it makes sense. But some people take to the extreme, right? They always think in terms of possibilities, you know. They create too many abstractions sometimes, right? So I think that kind of like also complicates the software unnecessarily because sometimes what they predict doesn’t really happen. And some people have this principle, YAGNI, right? You ain’t gonna need it. So how to find the balance between good enough abstractions and maybe too many abstractions?
Mauricio Aniche: Yeah. But the best there would be to have a crystal ball, right? So we could predict, precisely, what will change and what will not change. And we make mistakes for both sides, right? So we sometimes over design and we create something more complex for something that doesn’t have to be complex. And other times we under design. So we create something too simplistic. And I think of course, experience is something that helps as time goes by, you learn better to listen to your requirements, product manager person, and then understand if you need an abstraction there or not.
The other one is, you try to observe. So you start very simple. Don’t overcomplicate from the beginning unless you have a clear reason for it. And then as you go, then you add an abstraction. But something that I also say in my book, that is, I feel like we said so much, don’t over design, don’t over design, don’t over design. That I feel the main reason, the main problem right now is that we are under designing things more than over designing things, right?
So a joke that I always make with my friends was, I never saw a person saying, you know, calling the wife or the husband or the significant one and saying, hey honey, I’m stuck here at job because you know, this code is full of beautiful interfaces and etc. It’s usually the other way around, right? Hey honey, I’m late because this code has 3000 lines of code with no single abstraction that I can understand. I have to guess them from the implementation. So I also feel we, we are always expanding, you know, we go to the extremes, we got to find the midterm there. So, and I even say this explicitly in the book, don’t be afraid of sometimes doing over design because if you feel like this is going to need an extension point in the future, go for it. Don’t be afraid of that.
Henry Suryawirawan: Yeah, like you mentioned, maybe we do need a crystal ball, right? But I think the essence of your advice here, don’t be afraid, right? Sometimes, I think we will make mistakes in terms of our abstractions. The key here is to refactor it, right? Make changes whenever you understand the problem better and you find a better way of designing it, right? And again, having good test cases that covers not just the unit test but also maybe integration or acceptance test, can actually help you in order to make these redesigns.
[00:41:05] Principle #5: Handling External Dependencies and Infrastructure
Henry Suryawirawan: So the next is about handling external dependencies or infrastructure. So things like database calls, web service, or maybe other things that you depend on. So why do you think we should manage all these external things better?
Mauricio Aniche: Yeah. Good question. So no systems live in the vacuum, right? At some point, you’re going to have to integrate this to another system, being it a database or web service and so and so forth. And so you have to find ways to connect your beautiful object-oriented design to an external system that speaks a different language, that has a different abstraction than you have. If you don’t think so much about it, and let’s say you just code everything in one big class and etc, suddenly you’re very dependent on that abstraction, on that system. If that system changes, it’s going to propagate a lot of changes to you, harder to test, so and and so forth. So you have to explicitly think about external systems, external dependencies, and how you handle them.
And I give a few tips in the book. And the first one is, I find it quite simple actually to do, is separate infrastructure from domain codes. Whenever you’re going to have to talk to a third party, let’s say a database, just don’t do this in the same place you do business rules. Just create another class whose only responsibility is to translate your domain entity to a database call, let’s say. Move it to somewhere else. Don’t let them stay together. That will already improve a lot. If there’s a change in your database, you just have to change one point, right? You just go to your database class and you just change there. Your entities and your domain is more testable because it’s easier for you to replace that small part of the system that is a database with a mock, for example, so only advantages.
There, I also make a discussion, that is because I feel like sometimes you read books, people on the internet saying you should hide the external infrastructure as much as possible from everywhere else. Like your code should not know you’re using a relational database, your code should not know, you know, that you’re using them, NoSQL database, whatever. And I feel like this is such a bad advice, right? Because if you really want to write something that is fully agnostic of the many technologies you have to choose to deliver software, you’re going to have to write so much code. And it’s way more complex and way more likely to have a bug and probably doesn’t use your infrastructure to the best, because if you create something agnostic, you’re sort of, you know, leveling down everything else.
And I understand where this comes from, right? Because back to my first point here, you don’t want to make your code really coupled to the external party. But there’s a limit there. And usually my argument in the book that I make is what you need to do is you have to hide those decisions from the code, but not from the developer that is maintaining that code, right? So the developer knows it’s, you know, a relational database behind the scenes. So, you know, you can use all the advantages that relational databases give to you. But if you look at the code, the code of course knows it’s a relational database, but a lot of it is a little bit abstracted away. So you just make a call, you know, my repository.save() something. You hit this from the client code a little bit, but you’re not really hiding it completely. The developer knows it’s really clear that it’s a relational database behind the scenes.
So that is my main point there that you have to separate these things, but you’re not pretending you don’t know them. You know them and it’s good that you know them because you can take optimal decisions, right?
Henry Suryawirawan: Yeah, so I think that’s a very, very important point, right? Separate infrastructure from domain code or your business logic, right? So I think, again, coming back to domain-driven design, this is one of the key principles as well. And I find that design patterns such as hexagonal or ports and adapters kind of a pattern, right? Fits nicely into this, right? If you follow that principle along, so you will tend to separate the infrastructure from the business domain. And I think, yeah, what you mentioned about don’t try to hide too many things, right? Because sometimes, especially in my previous experience, right? Sometimes even your relational database, right, could use multiple read replicas, right? And hence, it becomes eventual consistency. So I think if you try to hide it, it’s very, very difficult, right? And hence, I think it’s a very, it’s actually okay to actually, reveal this. But maybe not in your business domain logic, right? So I think try to create an abstraction such that the developers can actually understand what is going on.
[00:45:12] Principle #6: Achieving Modularisation
Henry Suryawirawan: So the last one is about modularization. So I think maybe once you create an aggregate or bounded context, right, you probably create a module or sometimes also packages, right, if you want to distribute your SDK or something like that. So I think one very, very useful tips that you write in the book is about deep modules. So tell us what is this deep module is all about.
Mauricio Aniche: Yeah. So the chapter of the book was there to cover the problem that is, sometimes, your different domains, they grow very much, right? And then maybe back to the example of billing. And then maybe let’s say billing is dozens of classes to make billing, and then they also have text. And then maybe you also have systems that, you know, notify people and send them invoices via email and whatever. And all those domains, they grow very fast. Maybe you even want different teams working on them because you’re that big right now, you know. How do you make these modules to communicate with each other? Because although they are isolated, they still depend on each other, you know? Billing might send a message to text. And billing might send a message to notifications, whatever.
So this is to me, the moment where you need modularization. And I copied the term deep modules from John, from the Philosophy of Software Design, because I really loved that term. But I think I rephrased it a little bit to my own point of view. That is, I feel like something we want to for sure combat is that idea that’s everything needs to be a module, right? You don’t want to have 1 million modules or 1 million microservices, because it also does, whenever you split stuff, you win something, but you also lose something. So you want to find the right trade-off there.
And deep modules is basically the idea that what you want is, a module needs to do something very “big”, so to say in quotes. Right, it has to have like a beefy responsibility. Like for example, this is the entire billing domain, this is one module that I have. So it is not just one class, you know, one class should not be a module in most cases. So it does something really beefy, really important for the business, and it also offers a very simple layer that simplifies all the complexity that is inside of that module. This is the type of module you want to have in your system, right? Complex stuff that is exposed to the outside in the simplest way possible. So that is the idea, so that the clients, others that want to communicate with your module, they have to do the least amount of effort to be able to send you a message.
And I think this is where, to be honest, where the challenge starts. Because it’s creating a simple interface on top of something very complex is tricky. I can’t remember now if it’s in my book or in John’s book, or maybe in both, that there are ideas, you know, like come up with sensible defaults, right? You don’t have to force your clients to have to configure every single possible variable that you have in there. So for example, this is one example. Simplify things, and this is where you need design again, right?
Henry Suryawirawan: Yeah, so I think deep modules here, I really like it, right? Because sometimes when we create modules, we don’t really think about the interfaces of the API or the contracts, right? So I think the key essence here is like simple interface, so you abstract away all the inner details by exposing those simple interface. And yeah, in fact, the complexities is hidden inside the module. And I think a few other things that I also like in the modularization part is actually think of like the module. Even though you build it within your own team, right? You should think as if like you’re going to publish it to another team or another company which is beyond your control and you cannot kind of like give them a close guidance. And the last thing, of course, like don’t leak out the inner details. Maybe like your database tables, columns, and things like that. So I think that’s definitely a bad practice.
So Mauricio, thank you for giving us the walkthrough of the six principles. Again, there are so many things in your book. So for people who love this conversation so far, I’m sure you’ll get a lot more insights by reading the book. Again, the book is not a thick book, right? So probably it’s like 150 pages, like what Mauricio said. You can actually finish it in a few days. So please do check it out. I think I find this book, even though it sounds simplistic, but looking at my career experience, right, this is not something that is commonly practiced.
[00:49:18] Owing to Junior Developers
Henry Suryawirawan: So I think in the last part of your book, you also mentioned about being pragmatic. So one particular thing that I love about being pragmatic is, you talk about for us to do this simple object-oriented design, because we owe this to our junior developers. I find this quite beautiful message. So maybe if you can enlighten us a little bit about this.
Mauricio Aniche: For sure. So whenever you’re coding and you’re trying to be pragmatic, remember that at some point there will be a junior developer that will maintain this. So it is our duty to make sure they can learn from the code we’re writing, right? When I was a junior developer, I was learning a lot from what I was reading. So you owe this to the junior developer you’re directly or indirectly training. So that was sort of the gist there. I’m really happy you like this.
Henry Suryawirawan: Yeah. So I think the onus is on us, right? So every time we leave the code behind. Because the code tends to leave for quite some time, right? You don’t always rewrite everything every time. So I think the next developer who comes in and try to understand your code will refer to your code design. And most likely, they will just build on top. So if you have a messy code, obviously, they will learn a messy thing and maybe they will bring it forward to another project or software that they are working on. So I think the cycle continues and hence we probably have a bigger problem in the software industry.
[00:50:32] 3 Tech Lead Wisdom
Henry Suryawirawan: So thanks for this conversation, Mauricio. I have one last question, which I asked you last time in our conversation before, right? I call this the three tech leadership wisdom. So I’d like to hear again from you if you have a different wisdom this time.
Mauricio Aniche: Yeah, good question. And, I will try to give a suggestion based on the discussion today. So I guess it will be a bit different from the previous one. So the first one is focus on your technical debt, because at some point it can become impossible to pay. So everything that we discussed of refactor early, and etc, I truly believe it makes a difference in practice.
Then focus on quality. So, okay, that’s not so different from the first conversation we had, Henry, but I guess I like quality. Don’t let it be a second class citizen in the process, right? Find ways to make sure quality is just part of the process, part of the culture.
And finally, something that is a lot on my mind right now, given the team that I’m working with, is focus on developer experience. Because coding should be fun and fast, not boring and slow. But once you grow, it is very hard to keep things that way. So they have to put some energy there.
Henry Suryawirawan: Yes, so I think developer experience is top of mind. It’s quite a hot topic these days, right? So many publications around it. So definitely simple object oriented design or simple software design, right, will help a lot in terms of developer experience as well. Because code that is easily understandable, gives a lot of pleasure and creates flow whenever you work on that software. So for people who love this conversation, they want to check out with you online, or they want to buy the book, is their place where they can find you?
Mauricio Aniche: For sure, I’m on Twitter, X, my name and surname all together. Feel free to add me on LinkedIn. My website, mauricioaniche.com, has a lot of information there, including a link to the website of the book. Those are the main ones for sure that I use.
Henry Suryawirawan: All right. Thanks again, Mauricio, for this second episode. So I’m looking forward for next conversation that we have. So I’m sure we’ll learn, we’ll definitely learn a lot of things as well. So thanks again.
Mauricio Aniche: Thank you.
– End –