Hi and welcome back to Weekly Dev Tips. I’m your host Steve Smith, aka Ardalis.
This is episode 58, on the concept of boundaries, with guest James Hickey.
This week's tip is brought to you by devBetter.com.
Sponsor - devBetter Group Career Coaching for Developers
Need to level up your career? Looking for a mentor or a the support of some motivated, tech-savvy peers? devBetter is a group coaching program I started last year. We meet for weekly group Q&A sessions and have an ongoing private Slack channel the rest of the week. I offer advice, networking opportunities, coding exercises, marketing and branding tips, and occasional assignments to help members improve. Read some of the testimonials on devBetter.com and see if it sounds like it you might be a good fit.
This week we have our first returning guest, James Hickey. James was on the show earlier for episode 48 on how to accelerate your career. This week, he's back to talk about boundaries withing software systems. James is a software developer working remotely in eastern Canada. He's recently written a book about keeping your code clean called "Refactoring TypeScript" (https://leanpub.com/refactoringtypescript). He's also the author of an open-source .NET Core library called Coravel, which provides advanced capabilities to web applications. Welcome back, James!
Hi! I'm James Hickey.
I'm a software developer working remotely in eastern Canada.
When I started my career as a software developer I was thrust into a large codebase for a SAAS that helped automotive manufacturers perform analytics on their financial data.
The way the codebase was organized is probably familiar to most developers - especially those with a background in enterprise-y companies. The solution was organized into 3 main projects: business, DAL (data-access), and "core" (which was just a bunch of classes having no business logic full of public getters and setters).
At the end of the day, all the real business logic was mostly found within stored procedures in the database. So, all those layers didn't serve any real purpose. Business-oriented classes would just call a function from the DAL layer, and that method would call a stored procedure.
As a fresh-out-of-school developer who's trying to learn "how the pros do it", I didn't question this way of organizing code.
Eventually, though, I came to realize that this way of organizing code was terrible. It was hard to find code for specific features. You end up having to switch contexts between multiple projects when working on the same feature.
A Better Way
I've also been in projects having very different ways of organizing code, yet suffered from the same kinds of issues.
Throughout this time, I had a hunch that there was a common issue that was causing these difficulties. It didn't matter how well classes or sub-systems were designed, because, in the grand scheme of things, it was still hard to deal with the codebase as a whole.
As I read books and blogs and listened to well-known industry experts share their knowledge about software design, I came across better techniques and patterns for organizing code and designing software well.
Then, I discovered domain-driven design.
DDD is a pretty huge subject, but at the heart of the entire philosophy is the idea that the most important thing about managing complexity in software is around putting up boundaries.
In these other systems I've mentioned, the boundaries were enforced the wrong way. Instead of slicing our solutions by technical concerns (like by data-access, objects, interfaces, etc.), DDD teaches us to slice our solutions by business functionality (like shipping, search, billing, etc.)
Since then, I've had the opportunity to learn about other approaches to software design and have formed some opinions around what works well and what generally doesn't work out so well.
Out of all of these ideas, the most important one I've learned and have seen the effects of within real software projects is this idea of creating boundaries.
Different Boundary Types
You might be familiar with the concept called Bounded Contexts. In a nutshell, these are isolated sub-systems or bubbles that you design and build individually. Instead of creating one codebase and shoving all your code into it, you create a codebase or application per specific business feature or area of functionality.
Multiple boundaries can communicate with each other, but not by traditional means. In projects like the ones I mentioned at the beginning, if shipping needed information from the payments feature, it would just reach into the database and query the payments table!
These more strict boundaries mean you can't just reach into another feature's data or code.
This has many benefits. Mainly, it allows the inside of each boundary to attack its core business problem head-on and not worry about secondary concerns like persistence and what other business problems require. And it decouples all your different bubbles or contexts.
Inside each bounded context are these other boundaries called aggregates. These are objects that represent transactional boundaries.
The details are not important, but what is important is that each aggregate does not directly call another aggregate's methods and grab its data. Usually, aggregates will emit events to communicate with each other.
I use domain-driven design as the first example because the idea of boundaries is so fundamental to it.
But there are other ways to enforce boundaries.
Some prefer to create an isolated component, module, package or assembly (depending on what language you are in) and expose all the functionality of that isolated component as a facade. In this case, you might have one class that has all the publicly accessible behaviours or functions. None of the internal classes are exposed.
When looking at architectures like Clean Architecture, all business functionality might be exposed as use cases. Each use case, like "register new user", would be modelled as a single class. This class would not expose any domain objects or objects from modules farther down the chain. It would expose it's own specific models or DTOs. This is a way to enforce a boundary so that the outside world doesn't know about the internal details of specific modules or components.
Similarly, if you are building a web API then you might want to enforce boundaries by using view models or DTOs which are used for sending data to your clients. This way, internal details like specific domain classes aren't exposed and you can modify or version each endpoint without affecting the other modules or projects that depend on it.
Using specific classes dedicated for use in HTTP POST data binding also helps keep boundaries around each specific end-point. You also get the added security benefit of not "over posting."
Whenever you share code you are introducing some form of coupling. This, in turn, is the opposite of putting up boundaries.
Let's say, for example, I have a User class. This class is used within the user profile logic and the authentication logic for an application.
If I need to add new behaviours to the authentication flow, does it make sense that the same functionality is now available for use in the user profile scenario? Since both features are sharing the same class, this is possible.
This approach of trying to share as much code as possible throughout our apps is what causes spaghetti code and bugs galore. This is probably the biggest issue I come across in codebases. We think that sharing everything is good. But it's not. It creates a tangled mess of dependencies that, over time, cause businesses who want to be agile to sluggishly attempt to keep up with customer and market needs.
Instead, if we isolate each of these features and NOT share that User class, then changes from one feature won't affect the other.
Sure, you might end up creating two different user classes that have what look like duplicated fields, but that's OK. You aren't duplicating logic because these classes represent different things. The logic for the user profile screen is going to be different than authentication logic by definition.
There is a place for shared behaviour, like sharing how you might display a user's name in your application's UI. But fundamentally, we should seek to create boundaries around the different parts of our codebases.
Next time you find yourself having to start a new project or product, think about how you can isolate that product or feature from the rest of your codebase.
Maybe you want to build it as a completely separate assembly or project? In this case, you could use an event-driven means of communication. Or, maybe expose a public API as a facade.
If that doesn't make sense or isn't possible, you can at least create a new folder structure that makes it very clear what business functionality exists in that place.
I've written more about this last point over at builtwithdot.net if you are curious.
Thanks for listening in!
Thanks, James. I've added a link to your blog to the show notes. Listeners interested in learning more about Domain-Driven Design and Clean Architecture will find additional resources in the show notes as well.
Show Resources and Links
Changing How Your Code is Organized Could Speed Development
Clean Architecture on GitHub
That’s it for this week. If you want to hear more from me, go to ardalis.com/tips to sign up for a free tip in your inbox every Wednesday. I'm also streaming programming topics on twitch.tv/ardalis most Fridays at noon Eastern Time. Thank you for subscribing to Weekly Dev Tips, and I'll see you next week with another great developer tip.