Boundary-Driven Development Dennis Doomen @ddoomen | Principal Consultant | Microsoft MVP Oredev ‘24
About Me Coding Architect in the .NET space with 28 years of experience on an everlasting quest for knowledge to build the right software the right way at the right time
How do you prevent too much coupling?
Increase cohesion and decrease coupling using abstractions…
…but that often leads to too many abstractions
And how do you increase cohesion?
By applying DRY (Don’t Repeat Yourself)
But DRY creates more coupling
Then what is the solution?
Understand the internal boundaries
Controlling dependencies on the architecture level
Adopt an architecture style that embraces the DIP Order Processing IStoreOrders<T> + Query<T>(); + Add<T>(); + Delete<T>(); NHibernate Repository Order Processing IStoreOrders + GetIncompleteOrders(minValue); + StoreOrder(); + CompleteOrder(); OrderRepository VS
Onion Architecture Domain Model Dependencies
Hexagonal Architecture (a.k.a. Ports & Adapters) Domain Application Primary Port Primary Port Secondary Port Secondary Port Web Adapter API Adapter Database Adapter Adapter Dependencies Dependencies
Clean Architecture Entities Dependencies
Organize by functionalities / capabilities
Apply DRY within those “boundaries” Duplicated Service 1 Duplicated Service 1 Duplicated Service 2 Duplicated Service 2 Duplicated Service 1 Centralized Service 3 Extension Methods Extension Methods Extension Methods Extension Methods Helpers Helpers Helpers Helpers
Service Service Service Service Service Centralized Service Align your test scope with those “boundaries” b o u w e l
Controlling dependencies on the package level
Main Package Application Bunch of blocks that can be used directly Uses composition over inheritance Convenience blocks that don’t hide the magic Shared Package Contract Contract Only depend on more abstract packages… Stable Package …or depend on more stable packages Auxiliary Package Blocks that are not used together do not belong together Optional Dependency Dependency Package Consumers should not be faced with optional dependencies No cyclic dependencies Principles of successful package management
Controlling dependencies on the code level
Reduce code visibility Remove unused code
Avoid technical folders and organize code by functionalities / capabilities Functional Folders “Unit” of (integration) testing DRY within boundaries Can be used to clarify “public” parts Only unit tested for specific reasons Role-based interface name
Treat adjacent folders as separate boundaries
Use the Tell, Don’t Ask principle
Use the Law of Demeter to detect unnecessary coupling
Encapsulate primitive types and collections
DI is great, but avoid a global container Local DI container Local DI container Local DI container No DI container needed No DI container needed.
It’s fine to inject concrete classes inside boundaries
Understanding the current design
Inspect the project dependencies, folder names and namespaces
Use Type Dependencies Diagram in Rider/R#
Use NDepend
Use your IDE
Let AI tooling explain the code
Dealing with legacy code
My typical improvement flow Understand the production environmen t Understand the code base Find dead and unused code Build a safety net Improve deployability Improve code quality Improve code design Improve architecture
Create a safety net (e.g. Characterizations Tests)
Characteristics Test Suite Add characteristics tests Application Legacy Code Base Production Backup
Use Test Containers Characteristics Test Suite Application Legacy Code Base Linux Test Container SQL Server
Find existing seams and decouple them 1. Install a tool to visualize code 2. Identify modules or functional slices 3. Identify types that are supposed to be used together 4. Identify types are designed to be reusable 5. Find IoC registrations and verify 2 and 3 6. Assume code in adjacent folders to be independent
Use your learnings to visualize the target architecture I t a ll s t a r
Select a candidate to untangle first Application Legacy Entangled Capability
Start to untangle 1. Move code to functional folders 2. Apply code-level guidelines 3. Use DIP adapters 4. Duplicate code that isn’t supposed to be shared 5. Duplicate code that is used in multiple boundaries 6. De-duplicate code that is reusable and complicated 7. Considering moving to local IoC containers. IStoreOrders<T> + Query<T>(); + Add<T>(); + Delete<T>(); NHibernate Repository Order Processing IStoreOrders + GetIncompleteOrders(minValue); + StoreOrder(); + CompleteOrder(); Adapter
Application Legacy Extracted Capability Another Entangled Capability Rinse and repeat.
Find me at twitter: ddoomen mastodon: @ddoomen.mastodon.social bluesky: @ddoomen.bsky.social email: dennis.doomen@avivasolutions.nl slack: fluentassertions.slack.com

Using Boundary-Driven Development to beat code complexity