Introducing Bounded Contexts in a monolithic application - Robert Baelde - DDD Europe 2022
Introduction to Bounded Context in Monolithic Applications
The Challenge of Complexity in Monolithic Applications
- The speaker introduces the topic of implementing bounded context within a monolithic application, emphasizing the need for rapid value delivery in new projects.
- As projects grow, complexity increases with more features and team members, leading to a decline in productivity despite initial success.
- Productivity can decline exponentially as complexity rises, creating challenges for developers who struggle to manage the mental load of understanding the entire application.
- Developers face difficulties predicting the impact of changes during planning sessions due to hidden complexities within the codebase.
- Onboarding new engineers becomes challenging as they encounter an intertwined codebase without clear starting points or responsibilities.
Implicit Expertise and Problem Solving
- Implicit expertise complicates project management; it's often unclear who has knowledge about specific aspects of the application, making feature planning difficult.
- To address these issues, developers explore Domain-Driven Design (DDD), which advocates for introducing boundaries within applications.
Understanding Bounded Context
- A bounded context is defined as an independent and loosely coupled component that communicates minimally with others while maintaining its own data and language.
- Each bounded context should have explicit ownership assigned to individuals or teams responsible for its maintenance and development.
Coupling vs. Loosely Coupled Contexts
- Strongly coupled contexts lead to complex interdependencies and excessive communication, resembling "spaghetti" architecture; merging them may be necessary if they are too similar.
- In contrast, loosely coupled contexts allow for independent functionality where components can operate without relying heavily on one another.
Ubiquitous Language Across Contexts
- Ubiquitous language is crucial; different departments may interpret concepts differently based on their roles—illustrated by how various teams at Amazon view a "book."
- This variation highlights how understanding different perspectives can help identify bounded contexts effectively.
Data Sharing and Contracts Between Services
- Sharing data between services creates implicit contracts that complicate interactions; each context must guard its own data to maintain integrity.
Introduction to Microservices and Bounded Contexts
The Initial Experience with Microservices
- The speaker recounts their first job at a small startup where the advice was to start with microservices, which led to an unreliable application demo featuring an error page.
- Despite the potential of microservices, the experience highlighted that they can lead to issues if not properly managed, especially for developers unfamiliar with them.
Challenges of Implementing Microservices
- Microservices are beneficial primarily in large organizations equipped with adequate resources and expertise; smaller entities often lack the necessary knowledge or time.
- High upfront design costs are associated with microservices due to the need for defining boundaries and communication methods; poor design can result in inefficiencies.
- Changing boundaries within microservice architectures is costly and complex, requiring significant refactoring efforts when initial designs prove inadequate.
Productivity vs. Complexity in Microservices
- A graph illustrating productivity versus complexity shows that while productivity may initially drop due to boundary adjustments, it can stabilize post-refactor—this is manageable for larger organizations but problematic for rapid validation needs.
Communication Patterns Between Bounded Contexts
- The speaker emphasizes that it is possible to introduce boundaries effectively within monolithic structures by utilizing messaging or direct calls through well-defined interfaces.
- Messaging allows loose coupling between contexts since consumers only need to understand event schemas without needing details about their origins.
Handling Messages Effectively
- Storing messages can simplify future context introductions by avoiding complex migrations; having a history of messages aids in state reconstruction.
- Asynchronous handling of messages is recommended to prevent system slowdowns caused by synchronous processing of multiple events triggered by a single action.
Code Examples and Practical Implementation
- An example involving an "order placed" event illustrates how events can be published on a message bus, allowing consuming contexts to react appropriately without tight coupling.
Understanding Contexts in Software Development
The Importance of Providing Context
- The order context is crucial; without it, challenges arise. A providing context exposes an interface that informs the consuming context about available methods and expected return types.
- The consuming context only needs to understand the interface, not the implementation details, which are provided at runtime through dependency injection.
Implementation Example
- An example involves a public user repository with methods to retrieve user data by ID. This highlights how interfaces define expected interactions.
- Tests for the consuming context can be conducted independently of the providing context's implementation, allowing for effective mocking against contracts.
Steps to Introduce Context in Existing Monoliths
- To refactor an existing monolith, start by creating a context map to design before coding. This step is essential for clarity and organization.
- Move classes to their respective contexts using IDE tools that facilitate refactoring while ensuring namespace changes are applied throughout.
Detecting Dependencies Between Contexts
- It's important to identify where one context depends on another's classes to avoid tight coupling. Static code analytics can help detect these dependencies.
- A well-defined context map results from collaborative workshops and illustrates how different contexts interact within the system.
Refactoring for Improved Structure
- After moving classes into appropriate contexts, a clearer structure emerges, reducing interdependencies and improving maintainability.
- Utilize static code analysis tools (e.g., PHPStan) to measure cross-context usage and coupling levels between different contexts.
Measuring Coupling and Communication Patterns
- Analyze communication patterns between contexts; excessive direct calls or message exchanges may indicate poor boundaries or design flaws.
- Set measurable targets for reducing coupling over time as part of team goals, fostering better architectural practices.
Advantages of a Modular Monolith
- Establishing clear boundaries between contexts enhances development efficiency and reduces complexity in managing dependencies.
Microservices vs. Modular Monoliths: Finding the Right Balance
Advantages of Modular Monoliths
- Modular monoliths allow for clear boundaries without the added complexity associated with microservices, simplifying DevOps processes.
- Redesigning boundaries within a modular monolith is more straightforward; developers can temporarily allow some coupling during refactoring before fully committing to new communication patterns.
- Setting up a modular monolith is generally easier and faster than establishing a microservice architecture, which often requires significant upfront investment in tools like Docker.
Transitioning Between Architectures
- Many companies oscillate between monolithic and microservice architectures due to past experiences, often leading to dissatisfaction with both approaches.
- A modular approach serves as an effective middle ground, allowing teams to manage productivity while gradually introducing necessary boundaries as needed.
Refactoring Strategies
- When productivity declines, it may be time to implement context mapping and design changes; this proactive approach helps maintain efficiency.
- The transition from a modular monolith to microservices can be manageable when done at the right time, leveraging existing communication patterns established during earlier phases.
Conclusion and Further Engagement