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