Getting Architecture Right: Part 2
Putting the pieces together
Let’s take a look at how we can apply sound architectural principles to design a modern, distributed, and resilient business application — so we can minimize the time, cost, and complexity of delivering and using it.
In Getting Architecture Right: Part 1, we separated thinking about software into a number of categories:
- Macro architecture (sometimes called high-level architecture) is where the primary concern is establishing the foundational attributes and principles that guide the selection of architectural patterns, detailed design, and implementation. The choices are Clean Architecture and Lean Architecture. In this example, we’ll use Lean Architecture.
- Architectural patterns are high-level, reusable solutions to common architectural challenges, providing logical templates for organizing and structuring software systems in a way that addresses specific concerns and promotes best practices. For this example, we’ll use composable services.
- Inter module communications enable the interaction and communication between various components or systems. The choices are procedure calls, remote procedure calls, messaging, and event publishing. For this example, we’ll use both messaging and event publishing.
- A programming paradigm is a fundamental style or approach to programming that guides how developers structure and write code. It encompasses the principles, concepts, and practices used to solve problems within a programming language. For this example, we chose object-oriented programming (OOP) and Java.
We can use Lean Architecture with composable services to weave parts of multiple architectural and inter-module communication patterns into a coherent application system that is highly functional, performant, economical, and responsive to evolving needs and feedback.
What’s the Application?
For this example, we’ll take a Web-based order entry application and connect it to the other business resources with which it interacts.
This is an application domain that most of us are familiar with. Even if we’ve never designed or built one before — most of us have used one or more different implementations when buying something on the Internet.
Follow this link to take a look at the requirements for our example application.
But First, Some Words About Domain-Driven Design
In this application example, we have chosen composable services as our primary architectural pattern. One of the keys to good services implementation is how you draw the boundaries for the individual services. We have found that Domain-Driven Design (DDD) is a great way to do that.
Without an effective way to break application requirements into manageable components, it is almost impossible to build a solid application on time and on budget.
Fortunately, DDD encourages close collaboration with application domain experts to model the system effectively and break it down into manageable components using bounded contexts.
Services Implement Bounded Contexts
Within each bounded context, aggregates are used to maintain business rules and ensure data consistency. Descriptive objects (value objects and entities) are used to represent the data with which the application works.
By focusing on the core domain and modeling it in a way that reflects real-world complexity, DDD helps create software that is more resilient to change, easier to maintain, and directly aligned with business needs.
Here are the key concepts:
- Domain: The business or real-world activity being modeled.
- Bounded Context: A clear boundary within which a domain model is consistent and defined. We implement them as services. If they are too big, we break them down into smaller contexts/services to keep the manageable and maintainable.
- Aggregate: A group of related entities and value objects, with an aggregate root enforcing business rules. We implement them as a service that manages a set of other services.
- Entity: A descriptive object with a unique identity and attributes that persist and are tracked over time. They are most typically persisted to databases. We implement them as persistent services that read entities from and write entities to databases.
- Value Object: A descriptive object without a unique identity, characterized by its attributes. We implement them as Java records. For example, persistent entities are made up of an identifier and a value object. We put all the entity field edits and cross-edits into its Java record so that it is impossible to instantiate an invalid entity.
Where Do We Start?
At its core, software development is about being really precise about what we mean. That’s pretty much all there is to it. Unfortunately, building applications gives us way too many opportunities to be imprecise — and that’s how most bugs are born.
So, let’s focus on the areas where being imprecise is most likely to bite us. After all, they are “bugs”.
- Data: Ensuring data integrity is a fundamental responsibility of application code. The data must be of the correct type, size, and contain only acceptable values. Without accurate and validated data, the application’s functionality becomes unreliable, as every operation and decision the system makes relies on the correctness of the data.
- Rules: or program logic, dictate how the application behaves under differing circumstances. These rules implement the business logic that drives the application’s core functions, transforming data into meaningful actions. Clear and well-defined rules ensure that the system behaves predictably and consistently, aligning with user expectations and business requirements.
- External Interfaces: External interfaces are the points of interaction between the application and other systems, such as databases, third-party services, or user interfaces. They handle the flow of information into and out of the application, ensuring smooth communication with the outside world. Designing robust interfaces is crucial to maintaining data consistency and enabling seamless integration, making the system adaptable to changes and new dependencies.
Applying Lean Architecture to a Distributed Order Entry System
In Part 1, we discussed Lean Architecture’s focus on simplicity, reducing waste, and delivering only what is necessary. Now, let’s see how we can use these principles to build a distributed, Web-based order entry system that effectively integrates with other business resources.
Composable Services: Building-In Flexibility
A key to designing this order entry application lies in its use of composable services. By breaking the application into modular components, each service becomes a self-contained unit responsible for specific functionality. This modular approach ensures that individual services can be developed, tested, and deployed independently, promoting faster iteration and reducing downtime during updates.
Designing the Core Services with DDD
Using Domain-Driven Design (DDD) principles, we’ll define clear service boundaries based on business requirements. The primary services might include:
- Order Management Service: To handle sales order creation, updating, and deletion.
- Inventory Management Service: To track product availability and update inventory based on order activity.
- Customer Management Service: To manage customer profiles, addresses, payment, and contact details.
- Product Catalog: To maintain product details like prices, descriptions, and availability.
- Payment Gateway: To manage payment processing for sales orders.
- Shipping: To handle shipping details and track delivery status.
Each service is designed to operate within its own bounded context, maintaining its business rules and ensuring data consistency.
Integrating Messaging and Event Publishing
To facilitate communication between these services, we’ll leverage messaging and event publishing. This approach allows our components to communicate synchronously and asynchronously, making the system more resilient and scalable. Here’s how it works:
- Messaging: Services use a message moderator to exchange messages and responses synchronously in real-time, enabling seamless data flow with failover, redundancy, and scalability.
- Event Publishing: Critical state changes, like order confirmations or inventory updates, are published as events that other services can subscribe to and react to dynamically.
This setup not only decouples services but also makes it easier to introduce new features without disrupting existing workflows.
Object-Oriented Programming for Business Logic
We chose object-oriented programming (OOP) to implement the core logic of our services. OOP’s principles — like messaging, encapsulation, and late binding — help us create modular, reusable code that aligns closely with the real-world business processes represented by our services.
For instance, using Java classes to represent entities (like Orders and Products) ensures that each object can handle its own logic and state. This approach makes the code more intuitive and aligns naturally with DDD’s emphasis on modeling real-world scenarios.
Ensuring Data Integrity and Rule Consistency
Data validation and business rules are enforced at the service level to maintain the integrity of the application’s core functions:
- Data Validation: Value objects (as Java records) are self-validating to ensure that all input data meets predefined criteria wherever it is processed, reducing errors and ensuring reliability.
- Business Rules: Each service adheres to specific rules that govern how it interacts with other components, ensuring that the logic remains consistent and aligned with business goals.
Building a Resilient and Adaptable System
By combining Lean Architecture with composable services, we create a system that is not only efficient but also resilient to change. As business needs evolve, we can adapt the application by adding, modifying, or replacing individual services without overhauling the entire system.
This approach not only minimizes the time, cost, and complexity of delivering new features but also ensures that the system remains responsive to customer feedback and market demands.
Wrapping Up
Designing a modern, distributed business application requires more than just the right technology — it demands a thoughtful approach to architecture that balances simplicity, flexibility, and scalability.
Lean Architecture and composable services, guided by DDD principles, provide a robust framework for building systems that meet these criteria, ensuring they are both effective today and adaptable for tomorrow.
If we’ve raised questions, or you disagree with something we’ve said, we’d appreciate hearing from you.
If this discussion was of interest to you, stay tuned for the next installment — where we’ll dive deeper into transactions, optimizing service interactions, and enhancing system performance.
If you found this article useful, a clap would let us know that we’re on the right track.
Thanks!
Suggested Reading:
- Getting Architecture Right: Part 1, What is it and how do we need to think about it?
- Composable Services, describes what makes a service composable.
- Designing with Composable Services, using composable services to optimize application design tradeoffs.
- Building with Composable Services, provides a more formal and detailed description of the composable services architectural pattern.
- The Magic of Message Orchestration, introduces the wiring that connects software components.
- Building Software Systems, breaking through the complexity of developing software.
- Designing a REST API, why messaging using state representations beats remote procedure calls.
- Why Architecture Matters — Part 1, why we should care.
- Why Architecture Matters — Part 2, more why we should care.