LEARN SOFTWARE ARCHITECTURE AND DESIGN
Learn Software Architecture & Design: From SOLID to CQRS
Goal: Deeply understand the principles of modern software architecture—from clean code and SOLID to Hexagonal Architecture, DDD, and CQRS—by building robust, maintainable, and scalable applications in C# and Java.
Why Learn Software Architecture?
Writing code that works is easy. Writing code that is easy to understand, change, and maintain over years is an art. Modern software architecture principles are the foundation of that art. They provide a structured approach to managing complexity, enabling teams to build resilient systems that can evolve with business needs.
After completing these projects, you will:
- Write clean, intention-revealing code.
- Apply SOLID principles instinctively to create flexible designs.
- Structure applications using Hexagonal Architecture to isolate core logic.
- Model complex business problems using Domain-Driven Design (DDD).
- Implement advanced patterns like CQRS and Event Sourcing for high-performance, scalable systems.
- Confidently build enterprise-grade applications in both C# (.NET) and Java (Spring).
Core Concept Analysis
The Journey from Clean Code to Advanced Architecture
The concepts you want to learn build on each other, forming layers of abstraction to manage complexity.
┌─────────────────────────────────────────────────────────────────────────┐
│ USER & EXTERNAL SYSTEMS (UI, APIs, DB) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼ Interacts via Adapters
┌─────────────────────────────────────────────────────────────────────────┐
│ HEXAGONAL ARCHITECTURE (Ports & Adapters) │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ APPLICATION CORE │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ DOMAIN-DRIVEN DESIGN (DDD) │ │ │
│ │ │ - Bounded Contexts, Aggregates, Entities, Value Objects │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ CQRS & EVENT SOURCING (Optional) │ │ │
│ │ │ - Commands -> Events -> State | Queries -> Read Models │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼ Built upon
┌─────────────────────────────────────────────────────────────────────────┐
│ SOLID PRINCIPLES & REPOSITORY PATTERN │
│ (The glue that holds the internal components together) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼ Foundation
┌─────────────────────────────────────────────────────────────────────────┐
│ CLEAN CODE │
│ (Meaningful names, small functions) │
└─────────────────────────────────────────────────────────────────────────┘
Key Concepts Explained
1. Clean Code & SOLID Principles
- Clean Code: Code that is easy to read, understand, and maintain. Think meaningful variable names, short functions that do one thing, and clear, concise comments (or no comments at all if the code is self-explanatory).
- SOLID: Five principles of object-oriented design that make software more understandable, flexible, and maintainable.
- Single Responsibility Principle: A class should have only one reason to change.
- Open/Closed Principle: Software entities should be open for extension, but closed for modification.
- Liskov Substitution Principle: Subtypes must be substitutable for their base types.
- Interface Segregation Principle: No client should be forced to depend on methods it does not use.
- Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Hexagonal Architecture (Ports and Adapters)
An architectural style that isolates the application’s core business logic from outside concerns (like UI, databases, or third-party APIs).
+-------------------+ +----------------------+ +------------------+
| Web Adapter |----->| IUserInputPort | | |
+-------------------+ | (Interface) | | Application Core |
+----------------------+ | (Business Logic) |
+-------------------+ | IDataStoragePort | | |
| Database Adapter |<-----| (Interface) | +------------------+
+-------------------+ +----------------------+
(Implementation) (Port) (The Hexagon)
- Ports: Interfaces defined by the application core. They define what the application needs, not how.
- Adapters: Concrete implementations of the ports. They connect the application core to the outside world.
3. Domain-Driven Design (DDD)
A methodology for building software that deeply models a complex business domain.
- Bounded Context: A boundary within which a particular domain model is consistent and well-defined. (e.g., “Sales Context” vs. “Support Context”).
- Ubiquitous Language: A shared language developed by developers and domain experts to talk about the system.
- Aggregate: A cluster of domain objects (Entities, Value Objects) that can be treated as a single unit. It has a root (Aggregate Root) which is the only entry point for modifications, ensuring consistency.
- Entity: An object with a distinct identity that persists over time (e.g., a
Customerwith a unique ID). - Value Object: An object defined by its attributes, not its identity. It is immutable (e.g., a
Moneyobject withamountandcurrency). - Repository Pattern: An abstraction that provides collection-like access to Aggregates, hiding the details of data storage.
4. CQRS (Command Query Responsibility Segregation)
A pattern that separates the part of the system that writes data (Commands) from the part that reads data (Queries).
+---------+ +----------------+ +-----------------+
| Client |----->| Command |----->| Write Database |
+---------+ | (e.g. AddItem) | | (Normalized) |
+----------------+ +-----------------+
|
▼ (Data Sync)
+---------+ +----------------+ +-----------------+
| Client |<-----| Query |<-----| Read Database |
+---------+ | (e.g. GetCart) | | (Denormalized) |
+----------------+ +-----------------+
- Commands: Change the state of the system, but do not return data.
- Queries: Retrieve data from the system, but do not change its state.
Project List
The following 11 projects will guide you from foundational principles to advanced architectural patterns.
Project 1: SOLID Refactoring Challenge
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java, Python
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Design Principles
- Software or Tool: .NET, Visual Studio/Rider or Java, IntelliJ/Eclipse
- Main Book: “Clean Code: A Handbook of Agile Software Craftsmanship” by Robert C. Martin
What you’ll build: You’ll take a provided “god class” (a large, messy class that does too much) and refactor it into a set of small, focused classes that adhere to SOLID principles.
Why it teaches architecture: SOLID principles are the bedrock of good architecture. This project forces you to break down complexity and create maintainable components before you even think about layers or patterns.
Core challenges you’ll face:
- Identifying responsibilities → maps to Single Responsibility Principle
- Adding new functionality without modifying existing code → maps to Open/Closed Principle
- Using interfaces to decouple components → maps to Dependency Inversion Principle
- Creating smaller, client-specific interfaces → maps to Interface Segregation Principle
Key Concepts:
- SOLID Principles: “Clean Architecture” by Robert C. Martin (Chapters 7-11)
- Refactoring Techniques: “Refactoring” by Martin Fowler (Chapters 1-2)
Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic C# or Java programming, understanding of classes and interfaces.
Real world outcome:
You’ll start with a single file containing a class that violates every SOLID principle (e.g., a ReportGenerator that fetches data from SQL, parses it, formats it for both PDF and HTML, and emails it). Your outcome will be a set of clean, decoupled classes (SqlDataReader, PdfFormatter, HtmlFormatter, EmailService, etc.) orchestrated by a main class, all depending on abstractions, not concretions. The program’s output (a generated report) will remain identical, but the internal structure will be vastly improved and easily testable.
// Before: One massive method in one class
// After: A console application that runs and prints:
// "Fetching report data..."
// "Formatting report as PDF..."
// "Emailing PDF report to manager@example.com..."
// "Report sent successfully!"
Implementation Hints:
- Start by identifying the different “reasons to change” in the god class. Each one is a candidate for a new class.
- Don’t be afraid to create interfaces, even if you only have one implementation at first.
- Use Dependency Injection (even if it’s just passing dependencies in the constructor) to wire up your new classes.
- Write simple unit tests for each new, small class to prove it works in isolation.
Learning milestones:
- Successfully extract one responsibility into a new class → Understand Single Responsibility.
- Introduce an interface and depend on it → Understand Dependency Inversion.
- Add a new feature (e.g., CSV export) without changing existing formatters → Understand Open/Closed.
- The entire program works, but is composed of many small, testable classes → You’ve internalized SOLID.
Project 2: Simple Layered CRUD API
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java
- Coolness Level: Level 1: Pure Corporate Snoozefest
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Software Architecture
- Software or Tool: .NET Web API, Entity Framework Core (or Spring Boot, Spring Data JPA)
- Main Book: “Patterns of Enterprise Application Architecture” by Martin Fowler
What you’ll build: A standard 3-layer REST API for a simple entity (e.g., a Product). The layers will be Presentation (API Controllers), Business Logic (Services), and Data Access (DAL).
Why it teaches architecture: This project establishes a baseline “classic” architecture. Understanding this traditional layered approach is crucial for appreciating why more advanced patterns like Hexagonal Architecture were invented. It teaches separation of concerns at a macro level.
Core challenges you’ll face:
- Defining clear boundaries between layers → maps to separation of concerns
- Passing data between layers (DTOs) → maps to data transfer objects vs. domain models
- Handling dependencies between layers → maps to dependency flow
- Wiring up dependencies with DI container → maps to Inversion of Control
Key Concepts:
- Layering: “Patterns of Enterprise Application Architecture” Ch. 2 - Fowler
- DTOs (Data Transfer Objects): “Patterns of Enterprise Application Architecture” - Fowler
- Dependency Injection in .NET/Spring: Official documentation
Difficulty: Beginner Time estimate: Weekend
- Prerequisites: Project 1, basic knowledge of REST APIs.
Real world outcome: A running REST API that you can interact with using a tool like Postman or Insomnia. You can create, read, update, and delete products.
POST /api/products
{ "name": "Laptop", "price": 1200 }
-> 201 Created
GET /api/products/1
-> 200 OK
{ "id": 1, "name": "Laptop", "price": 1200 }
The project structure will be clearly organized into folders/projects for each layer: Api, BusinessLogic, DataAccess.
Implementation Hints:
- Presentation Layer: Should only contain API controllers. Its job is to handle HTTP requests/responses and call the business logic layer. It should not contain any business rules.
- Business Logic Layer: Contains services that orchestrate data access and implement business rules (e.g., validation).
- Data Access Layer: Responsible for all communication with the database. Use an ORM like EF Core or Spring Data JPA.
- Use DTOs to transfer data between the API layer and the Business Logic Layer to avoid exposing your database entities directly.
Learning milestones:
- API endpoints work correctly → You can create and retrieve data.
- Business logic is in the service layer, not the controller → You have separated presentation from business rules.
- Database code is only in the data access layer → You have isolated data persistence.
- Changing the database schema doesn’t force a change in the API contract (DTOs) → You understand the value of decoupling layers.
Project 3: Introduce the Repository Pattern
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Design Patterns
- Software or Tool: .NET, EF Core (or Spring, Spring Data JPA)
- Main Book: “Domain-Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans
What you’ll build: You will refactor the Data Access Layer from the previous project to use the Repository Pattern. You’ll define repository interfaces in your business logic layer and implement them in your data access layer.
Why it teaches architecture: This is a critical step towards more advanced architectures. The Repository Pattern decouples your business logic from the specific data persistence technology (the ORM, the database). Your business logic now depends on an abstraction (IProductRepository), fulfilling the Dependency Inversion Principle.
Core challenges you’ll face:
- Defining generic or specific repository interfaces → maps to design trade-offs
- Implementing the interface with an ORM → maps to separating the what from the how
- Ensuring the business layer only references the interface → maps to Dependency Inversion
- Creating an in-memory repository for testing → maps to testability and decoupling
Key Concepts:
- Repository Pattern: “Domain-Driven Design” by Eric Evans, Chapter 6
- Dependency Inversion Principle: “Clean Architecture” by Robert C. Martin, Chapter 11
Difficulty: Intermediate Time estimate: Weekend Prerequisites: Project 2.
Real world outcome:
The API will function exactly as before. However, your project’s dependencies will now be inverted. The BusinessLogic layer will no longer depend on the DataAccess layer. Instead, both will depend on the repository interfaces (which are conceptually part of the business/domain layer). You will be able to prove this by creating a second implementation of IProductRepository (e.g., InMemoryProductRepository) and swapping it in your DI container to run the application without a real database.
// Dependency Flow
// Before: Api -> BusinessLogic -> DataAccess
// After: Api -> BusinessLogic <- DataAccess
// ^
// |
// (depends on IProductRepository)
Implementation Hints:
- Define your
IProductRepositoryinterface inside your Business Logic project/module. It should have methods likeGetByIdAsync,AddAsync,UpdateAsync, etc. - Your
ProductServicein the Business Logic layer should now take anIProductRepositoryin its constructor, not a database context. - Create an
EfCoreProductRepositoryclass in your Data Access Layer that implements theIProductRepositoryinterface. This class will contain the EF Core code. - To prove the decoupling, create a simple
InMemoryProductRepositorythat uses aList<Product>to store data. Configure your DI container to use this implementation and run your API. All endpoints should still work.
Learning milestones:
- Business logic compiles without a reference to the Data Access layer → You have successfully inverted the dependency.
- You can swap the real repository for an in-memory one → You have achieved true decoupling from the persistence mechanism.
- Your service classes are now easily unit-testable → You can pass a mock repository to test your business logic without a database.
- You understand that the repository represents a collection of domain objects → You are thinking in terms of the domain, not the database.
Project 4: Hexagonal Architecture To-Do List API
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: Java
- Alternative Programming Languages: C#, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Software Architecture
- Software or Tool: Spring Boot, Docker (or .NET, Docker)
- Main Book: “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” by Robert C. Martin
What you’ll build: A To-Do List REST API structured using Hexagonal Architecture (Ports and Adapters). You will clearly separate the “inside” of your application (the core domain logic) from the “outside” (the web framework, database, etc.).
Why it teaches architecture: This project teaches you to build systems that are independent of frameworks and external technologies. Your core application logic becomes a pure, testable, and reusable component that is not tied to Spring or .NET. It’s the practical application of the Dependency Inversion Principle at an architectural level.
Core challenges you’ll face:
- Defining the “hexagon” boundary → maps to identifying your core business logic
- Creating technology-agnostic ports (interfaces) → maps to defining application needs
- Implementing primary adapters (driving adapters) → maps to e.g., REST controllers
- Implementing secondary adapters (driven adapters) → maps to e.g., database repositories, email services
Key Concepts:
- Ports and Adapters: Original article by Alistair Cockburn
- Clean Architecture Layers: “Clean Architecture” by Robert C. Martin, Chapters 17-22
- Dependency Rule: “Clean Architecture” by Robert C. Martin, Chapter 16
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 3.
Real world outcome:
A running To-Do List API. The most important outcome is the project structure itself. You will have a core module/project with zero dependencies on any web framework or database library. You will have separate adapter modules for web and persistence. You can then demonstrate the power of this architecture by adding a new adapter—for example, a command-line interface (CLI)—that drives your application core without changing a single line of code in the core.
/todo-app
/core
/domain (TodoItem.java)
/ports/in (CreateTodoItemUseCase.java)
/ports/out (TodoItemRepository.java)
/service (TodoItemService.java - implements UseCase)
/adapters
/web (TodoController.java - Spring Boot)
/persistence (H2TodoItemRepository.java - Spring Data)
/main
(Application.java - wires everything together)
Implementation Hints:
- Start with the
core. Define your domain object (TodoItem). Define your “input ports” as use cases (e.g.,interface AddTodoItemUseCase). Define your “output ports” as repository interfaces (interface TodoItemRepository). - Implement your use cases in a service class within the
core. This service will depend on the output port interfaces. - Create a Spring Boot
webadapter. TheTodoControllerwill depend on your use case interfaces (AddTodoItemUseCase). - Create a
persistenceadapter. TheDatabaseTodoRepositorywill implement yourTodoItemRepositoryinterface and use a real database. - Use a main application/configuration file to wire up the adapters to the ports using Dependency Injection.
Learning milestones:
- The
coremodule has no external framework dependencies → You have successfully isolated your domain. - You can build and unit-test the
coremodule completely independently → You have a portable and testable business logic component. - You add a new type of adapter (e.g., a CLI) without modifying the core → You understand the power of ports and adapters.
- You realize your application is the core, and Spring/ASP.NET is just a delivery mechanism → A fundamental mind-shift in how you view frameworks.
Project 5: DDD-Lite - A Simple E-commerce Shopping Cart
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: Domain-Driven Design
- Software or Tool: .NET Console App (or Java Console App)
- Main Book: “Domain-Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans
What you’ll build: A console application that models an e-commerce shopping cart. You will focus exclusively on the domain model, creating an ShoppingCart Aggregate Root that protects its business rules (invariants).
Why it teaches architecture: This project is a focused dive into the tactical patterns of DDD. You’ll learn to think about behavior and consistency first, rather than just data. You’ll understand how to encapsulate complex business rules within a rich domain model, a core tenet of DDD.
Core challenges you’ll face:
- Designing the
ShoppingCartas an Aggregate Root → maps to defining transaction boundaries - Ensuring business rules (invariants) are always enforced → maps to e.g., cannot add a negative quantity, total cannot be negative
- Modeling
CartItemas an Entity andMoneyas a Value Object → maps to understanding the difference and their purpose - Exposing behavior, not state → maps to methods like
AddItem, notget/seton a list of items
Key Concepts:
- Aggregates, Entities, Value Objects: “Domain-Driven Design” by Eric Evans, Chapters 5-6
- Invariants: A rule that must be true at all times within an aggregate.
- Ubiquitous Language: Using terms like “AddItem” that make sense to the business.
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 4. Strong understanding of OOP.
Real world outcome: A console application that demonstrates the shopping cart’s behavior. The output will show the state of the cart after each operation and will throw exceptions when business rules are violated.
Creating new cart...
Cart created with ID: 123
Adding 'Laptop' (1 x $1200.00)...
Cart 123 contains 1 item(s). Total: $1200.00
Adding 'Mouse' (2 x $25.00)...
Cart 123 contains 2 item(s). Total: $1250.00
Updating quantity of 'Laptop' to 2...
Cart 123 contains 2 item(s). Total: $2450.00
Attempting to add 'Keyboard' with quantity -1...
ERROR: DomainException: Item quantity cannot be negative.
Cart state is unchanged.
Implementation Hints:
- Create a
ShoppingCartclass. This is your Aggregate Root. It should have a unique ID. - Create a
CartItemclass. This is an Entity within the aggregate. It will have its own ID (e.g., Product ID) and quantity. - Create a
Moneyclass. This is a Value Object. It should be immutable and containAmountandCurrency. Implement operator overloads for addition/subtraction. - The
ShoppingCartclass should have a private list ofCartItems. Do not expose a public getter for this list. - Expose public methods like
AddItem(productId, quantity, price),RemoveItem(productId),UpdateQuantity(productId, newQuantity). All business logic and invariant checks happen inside these methods. - If a rule is violated, throw a custom
DomainException.
Learning milestones:
- You cannot publicly modify the list of items in the cart → You are protecting the aggregate’s boundary.
- The
Moneyvalue object correctly handles currency and calculations → You understand the power of immutable value objects. - It’s impossible to get the cart into an invalid state → You are successfully enforcing invariants.
- Your code reads like a description of the business process → You are using the Ubiquitous Language.
Project 6: Full DDD Application with Persistence
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Domain-Driven Design / Persistence
- Software or Tool: .NET Web API, EF Core, PostgreSQL
- Main Book: “Implementing Domain-Driven Design” by Vaughn Vernon
What you’ll build: You will take the Shopping Cart domain model from the previous project and integrate it into a full Hexagonal Architecture API. You will implement a repository that can save and load the ShoppingCart aggregate to and from a database.
Why it teaches architecture: This project bridges the gap between a rich domain model and the real world of persistence. You will face the challenge of mapping your complex, behavior-focused aggregate to a relational database schema, learning techniques to do so without compromising your domain model’s integrity.
Core challenges you’ll face:
- Designing a repository for the aggregate root → maps to only load/save whole aggregates
- Mapping the aggregate to a database schema → maps to ORM configuration for entities and value objects
- Handling transactions → maps to ensuring the aggregate is saved atomically
- Rehydrating the aggregate from the database → maps to reconstructing the domain object from data rows
Key Concepts:
- Aggregate Persistence: “Implementing Domain-Driven Design” by Vaughn Vernon, Chapter 10
- Repositories: “Implementing Domain-Driven Design” by Vaughn Vernon, Chapter 12
- Unit of Work Pattern: Often used with repositories to manage transactions.
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 5.
Real world outcome: A fully functional Shopping Cart REST API. You can create a cart, add items, and when you retrieve the cart later, its state is preserved.
POST /api/carts
-> { "cartId": "xyz" }
POST /api/carts/xyz/items
{ "productId": "p1", "quantity": 1, "price": 100 }
-> 200 OK
GET /api/carts/xyz
-> 200 OK
{
"cartId": "xyz",
"items": [ { "productId": "p1", "quantity": 1 } ],
"total": 100
}
The key insight is that your service/use case layer will look like this:
- Load aggregate from repository:
var cart = repository.GetById(cartId); - Call a method on the aggregate:
cart.AddItem(...); - Save the aggregate back:
repository.Save(cart);The service layer knows nothing about tables, columns, or SQL.
Implementation Hints:
- Use the
IShoppingCartRepositoryinterface from your domain layer. - Implement
PostgresShoppingCartRepositoryin your persistence adapter. - Use your ORM’s features to map the aggregate. In EF Core, you can configure
ShoppingCartas an entity and itsCartItemswith aHasManyrelationship.Moneycan be configured as an “owned entity” or “complex type”. - Your
Savemethod in the repository should handle both inserts and updates (an “upsert”). - Ensure all changes to an aggregate are saved in a single database transaction. The Unit of Work pattern (often built into ORMs like EF Core) helps with this.
Learning milestones:
- You can save an aggregate and its internal entities/value objects in one call → You are treating the aggregate as an atomic unit.
- When you load the aggregate, all its invariants are still intact → Your domain model is correctly rehydrated.
- Your domain model has no dependencies on the ORM → Your core logic remains pure.
- Your business logic is beautifully simple: load, operate, save → You have successfully separated domain logic from infrastructure concerns.
Project 7: CQRS Blog Platform (Command & Query)
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: Java
- Alternative Programming Languages: C#
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Software Architecture / CQRS
- Software or Tool: Spring Boot, Spring Data JPA, MapStruct
- Main Book: “Microsoft .NET - Architecting Applications for the Enterprise” by Dino Esposito and Andrea Saltarello
What you’ll build: A blog platform API that is explicitly split into a “write” side (Commands) and a “read” side (Queries).
Why it teaches architecture: This project introduces you to the fundamental concept of CQRS. You’ll learn that the model you use to change data doesn’t have to be the same model you use to read it. This separation allows you to optimize each side independently, a powerful technique for scalable applications.
Core challenges you’ll face:
- Designing Command objects → maps to capturing user intent, e.g.,
CreatePostCommand - Creating a separate, optimized read model → maps to denormalization for query performance
- Implementing Command Handlers and Query Handlers → maps to processing logic for each side
- Keeping the read and write models in sync → maps to data synchronization strategy
Key Concepts:
- CQRS: “CQRS” by Greg Young (original paper)
- Command and Query Separation: Bertrand Meyer’s principle.
- Read Models: “Designing Data-Intensive Applications” by Martin Kleppmann, Chapter 3
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Project 6.
Real world outcome: A running API with two distinct sets of endpoints.
Write Side (Commands):
POST /api/posts
{ "author": "douglas", "title": "My First Post", "content": "..." }
-> 202 Accepted
Read Side (Queries):
GET /api/posts
-> 200 OK
[ { "id": "1", "title": "My First Post", "authorName": "douglas" } ]
GET /api/posts/1
-> 200 OK
{ "id": "1", "title": "My First Post", "content": "...", "comments": [...] }
Notice the read model might be different from the write model (e.g., authorName vs authorId).
Implementation Hints:
- Write Side:
- Use your DDD Aggregate (
Post) from previous projects. - Create Command objects (e.g.,
CreatePostCommand). - Create Command Handlers that take a command, load the aggregate, execute the behavior, and save it.
- Use your DDD Aggregate (
- Read Side:
- Create simple DTOs or “read model” classes (e.g.,
PostSummaryDto,PostDetailsDto). - Create Query Handler classes that directly query the database (using Dapper, JDBC, or a lightweight ORM feature) and build the read models. Bypass the full Aggregate for performance.
- Create simple DTOs or “read model” classes (e.g.,
- Synchronization: For this project, a simple approach is fine. After a command handler saves the aggregate, it can publish an event (in-process) that a listener picks up to update the read model in the same transaction.
Learning milestones:
- You have separate models for writing and reading → You have broken the single-model mindset.
- Your queries are fast because they are simple, direct database reads → You understand the performance benefit of CQRS.
- Your command side is robust and transactional, focused on business rules → You understand the consistency benefit of using Aggregates for writes.
- You can change the read model (e.g., add a new field) without touching the write model → You have achieved true separation.
Project 8: Event Sourcing: Bank Account Ledger
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java, F#
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 4: Expert
- Knowledge Area: Event Sourcing / Advanced DDD
- Software or Tool: .NET, EventStoreDB (or Kafka, or even a simple PostgreSQL table)
- Main Book: “Designing Data-Intensive Applications” by Martin Kleppmann
What you’ll build: A simple bank account API where the state of an account is not stored directly. Instead, you will only store the sequence of events that happened to it (e.g., AccountCreated, MoneyDeposited, MoneyWithdrawn). The current balance is calculated by replaying these events.
Why it teaches architecture: This project teaches the principles of Event Sourcing, the logical conclusion of CQRS. You learn that “state” is just a left-fold of events. This provides a full audit log, enables powerful new features (e.g., “what was the balance at this point in time?”), and makes your domain logic even purer.
Core challenges you’ll face:
- Modeling business operations as events → maps to thinking in the past tense
- Saving events instead of state → maps to append-only persistence
- Rehydrating an aggregate from its event stream → maps to functional state reconstruction
- Handling versioning and idempotency → maps to ensuring reliability
Key Concepts:
- Event Sourcing: Martin Fowler’s article on Event Sourcing.
- State as a Fold: Functional programming concept.
- Projections: Creating read models from an event stream.
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Project 7.
Real world outcome: An API for a bank account. When you query the account, the system reads the event stream, calculates the current balance, and returns it.
You will also have an “event store” (which can be a simple database table for now) that looks like this:
| AccountId | Version | EventType | Data |
|---|---|---|---|
| acc-123 | 1 | AccountCreated | { “owner”: “Douglas” } |
| acc-123 | 2 | MoneyDeposited | { “amount”: 100 } |
| acc-123 | 3 | MoneyWithdrawn | { “amount”: 25 } |
The current balance is 100 - 25 = 75, but you never stored the number 75.
Implementation Hints:
- Your
BankAccountaggregate will have a method likeApply(IEvent)which modifies its state based on an event. - Your command methods (
Deposit,Withdraw) will not change state directly. Instead, they will validate the command and, if successful, create andApplyone or more new events. The aggregate keeps a list of “uncommitted events”. - Your repository’s
Savemethod will take the aggregate, extract the uncommitted events, and append them to the event store. - Your repository’s
GetByIdmethod will read all events for an aggregate from the store and replay them one-by-one on a new instance of the aggregate to restore its state.
Learning milestones:
- Your database contains no ‘state’ tables, only an append-only event log → You have fully embraced Event Sourcing.
- You can reconstruct the state of any aggregate at any point in time → You have unlocked the power of temporal queries.
- Your domain logic is a pure function:
(state, event) -> new state→ Your business rules are supremely testable and easy to reason about. - You build a “projection” into a separate read model table for fast queries → You have connected Event Sourcing back to CQRS.
Project 9: Build a Tiny Message Broker/Event Bus
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: Go
- Alternative Programming Languages: Java, C#
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: Distributed Systems / Messaging
- Software or Tool: TCP Sockets, Protocol Buffers/JSON
- Main Book: “Enterprise Integration Patterns” by Gregor Hohpe and Bobby Woolf
What you’ll build: A simple, in-memory (or file-based) message broker that supports publish-subscribe and queueing semantics. It will allow different parts of your application to communicate asynchronously.
Why it teaches architecture: This demystifies tools like RabbitMQ or Kafka. Building one, even a simple version, teaches you about asynchronous communication, decoupling, and the challenges of message delivery guarantees. It’s a key component for building event-driven architectures and microservices.
Core challenges you’ll face:
- Designing a simple wire protocol → maps to how clients and server communicate
- Implementing publish/subscribe logic → maps to managing topics and subscriptions
- Handling concurrent connections → maps to multithreading/goroutines
- Deciding on message delivery guarantees → maps to at-most-once vs. at-least-once
Key Concepts:
- Messaging Patterns: “Enterprise Integration Patterns”, Ch. 2-3
- Socket Programming: “The Sockets Networking API” (UNIX) or language-specific guides
- Concurrency: Go channels, Java
java.util.concurrent, C#System.Threading.Tasks
Difficulty: Expert Time estimate: 1 month+ Prerequisites: Project 8, strong networking and concurrency concepts.
Real world outcome: A server process that listens on a TCP port. You can write separate client programs that can:
- Connect and subscribe to a topic (e.g.,
order_created). - Connect and publish a message to that topic.
- The subscriber client will receive and print the message.
You can then use this tool to connect the “write” and “read” sides of your CQRS project asynchronously.
Implementation Hints:
- Define a simple JSON-based protocol:
{"command": "PUBLISH", "topic": "...", "payload": "..."}. - The server will maintain in-memory maps:
map<string, []client>for topics and subscribers. - When a publish command comes in, the server iterates through the subscriber list for that topic and sends the message to each client.
- Use a thread-per-client model to handle connections. Use channels or concurrent queues to pass messages between threads safely.
- For persistence (to survive restarts), you can append every message to a log file. On startup, replay the log to restore the state.
Learning milestones:
- A publisher and subscriber can communicate through the broker → You have a working pub/sub system.
- Multiple subscribers can receive the same message → You understand one-to-many delivery.
- The broker can handle multiple concurrent clients without crashing → You have handled concurrency correctly.
- You can restart the broker and it retains messages (if you implement persistence) → You understand durability.
Project 10: Modular Monolith for a Booking System
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: C#
- Alternative Programming Languages: Java
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 4: Expert
- Knowledge Area: Software Architecture / DDD
- Software or Tool: .NET, EF Core, Your message broker from Project 9 (or an in-memory bus)
- Main Book: “Fundamentals of Software Architecture” by Mark Richards and Neal Ford
What you’ll build: A single application (a monolith) for a hotel booking system, but internally structured as a set of highly-decoupled modules (Bookings, Payments, Users). These modules will communicate asynchronously using events.
Why it teaches architecture: This is the pinnacle of pragmatic, modern architecture for many businesses. It gives you the development simplicity of a monolith but the clean boundaries and decoupling of microservices. It teaches you how to manage complexity in a large application without the operational overhead of a distributed system.
Core challenges you’ll face:
- Defining Bounded Contexts → maps to identifying your modules (Bookings, Payments)
- Enforcing module boundaries → maps to preventing direct calls between modules
- Implementing asynchronous communication between modules → maps to using an event bus/message broker
- Sharing data between modules → maps to deciding what data is replicated vs. queried via API
Key Concepts:
- Modular Monolith: Martin Fowler’s article on the topic.
- Bounded Contexts: “Domain-Driven Design” by Eric Evans, Chapter 14.
- Event-Driven Architecture: “Designing Data-Intensive Applications”, Ch. 11.
Difficulty: Expert Time estimate: 1 month+ Prerequisites: Projects 1-9.
Real world outcome: A single, deployable web application for hotel booking. The project structure, however, will be composed of independent modules that could, in theory, be extracted into separate microservices later with minimal effort.
When a user books a room (Bookings module), a RoomBooked event is published. The Payments module listens for this event and processes the payment. The two modules are completely decoupled. You can prove this by temporarily disabling the Payments module; bookings can still be made, but payments won’t be processed.
Implementation Hints:
- Structure your solution with a separate project/module for each Bounded Context (
Bookings.Core,Bookings.Api,Payments.Core,Payments.Infrastructure, etc.). - Create a shared
EventBusinterface. Each module can depend on this to publish events. - Use a dependency injection trick or build rule to prevent projects from directly referencing each other’s internal classes. They can only communicate via the event bus or through public API contracts.
- The
Bookingsmodule might publish anRoomBookedevent containing thebookingIdandamount. - The
Paymentsmodule would have a handler that subscribes toRoomBookedevents, receives the event, and then processes the payment.
Learning milestones:
- Each business capability lives in its own isolated module → You have defined clear Bounded Contexts.
- Modules communicate only through asynchronous events → You have achieved high decoupling.
- You can develop, test, and even deploy one module without affecting the others → You have prepared your system for future scaling.
- You understand the trade-offs between a monolith, a modular monolith, and microservices → You can now make informed architectural decisions.
Project 11: Create a Pluggable Data Processing Pipeline
- File: LEARN_SOFTWARE_ARCHITECTURE_AND_DESIGN.md
- Main Programming Language: Java
- Alternative Programming Languages: C#
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 3: Advanced
- Knowledge Area: Design Patterns / Extensibility
- Software or Tool: Java, Spring Framework
- Main Book: “Design Patterns: Elements of Reusable Object-Oriented Software” by Gamma, Helm, Johnson, Vlissides (The “Gang of Four” book)
What you’ll build: A console application that processes a text file through a series of configurable steps (e.g., read file, count words, find most common word, write report). The key is that each step is a “plugin” that can be dynamically added, removed, or reordered at runtime.
Why it teaches architecture: This project is a masterclass in the Open/Closed Principle and pluggable architecture. You will learn to build a system that can be extended with new functionality without modifying the core logic, using patterns like Strategy, Chain of Responsibility, and reflection/service loading.
Core challenges you’ll face:
- Defining a common interface for all pipeline steps → maps to Strategy or Command pattern
- Creating a pipeline runner that orchestrates the steps → maps to Chain of Responsibility or Decorator pattern
- Discovering and loading plugins at runtime → maps to Java’s ServiceLoader or C#’s reflection
- Passing context/data between steps → maps to designing a shared context object
Key Concepts:
- Strategy Pattern: “Head First Design Patterns”, Chapter 1
- Chain of Responsibility Pattern: “Head First Design Patterns”, Chapter 9
- Java ServiceLoader API: Official Java documentation.
- Open/Closed Principle: “Clean Architecture” by Robert C. Martin, Chapter 8
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 3, strong OOP skills.
Real world outcome: A console application you can run with different configurations to perform different tasks.
# Run with default pipeline
$ java -jar pipeline.jar input.txt
Pipeline finished. Report: 'report.txt' contains 1250 words. Most common: 'the'.
# Run with a custom pipeline configuration
$ java -jar pipeline.jar input.txt --steps=read,uppercase,count,report
Pipeline finished. Report: 'report.txt' contains 1250 words. Everything is uppercase.
You can add a new step (e.g., a RemoveStopWordsStep.jar) to a plugins folder, and the application will be able to use it without recompilation.
Implementation Hints:
- Define an interface
IPipelineStepwith anexecute(PipelineContext context)method. - Create a
PipelineContextclass to hold the data that flows through the pipeline (e.g., file content, word counts). - Create several concrete implementations:
ReadFileStep,CountWordsStep,ReportStep. - Create a
PipelineBuilderthat reads a configuration and assembles the chain of steps. - Use Java’s
ServiceLoaderto discover availableIPipelineStepimplementations on the classpath, allowing for a true plugin architecture.
Learning milestones:
- You can assemble a pipeline of steps that process data sequentially → You understand the basic pattern.
- You can reorder the steps via configuration and the output changes accordingly → You have decoupled the orchestration from the implementation.
- You can add a new step as a separate JAR file and the application uses it → You have built a truly pluggable system.
- You have a deep understanding of the Open/Closed principle → You know how to build systems that welcome change.
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| 1. SOLID Refactoring | Beginner | Weekend | Foundational | 2/5 |
| 2. Layered CRUD API | Beginner | Weekend | Foundational | 2/5 |
| 3. Repository Pattern | Intermediate | Weekend | Foundational | 3/5 |
| 4. Hexagonal API | Intermediate | 1-2 weeks | Architectural | 4/5 |
| 5. DDD-Lite Cart | Advanced | 1-2 weeks | Domain Modeling | 4/5 |
| 6. DDD Persistence | Advanced | 1-2 weeks | Domain Modeling | 4/5 |
| 7. CQRS Blog | Advanced | 2-3 weeks | Scalability | 4/5 |
| 8. Event Sourcing | Expert | 2-3 weeks | Advanced Patterns | 5/5 |
| 9. Message Broker | Expert | 1 month+ | Systems Programming | 5/5 |
| 10. Modular Monolith | Expert | 1 month+ | Pragmatic Architecture | 5/5 |
| 11. Pluggable Pipeline | Advanced | 1-2 weeks | Design Patterns | 4/5 |
Recommendation
I recommend starting with Project 1: SOLID Refactoring Challenge. It’s a low-investment, high-reward project that builds the mental muscles required for all subsequent projects. It forces you to think about class design and responsibility, which is the absolute foundation.
After that, proceed sequentially from Project 2 through Project 8. This path is designed to layer concepts logically. Projects 9, 10, and 11 are more advanced and can be tackled once you are comfortable with the core DDD/CQRS ideas.
Final Overall Project
The final and largest project is Project 10: Modular Monolith for a Booking System. This project is the culmination of everything you’ve learned. It requires you to:
- Apply Clean Code and SOLID principles within each module.
- Use Hexagonal Architecture to structure each module, separating core logic from infrastructure.
- Define clear Bounded Contexts (Bookings, Payments, Users) as taught in DDD.
- Implement the Repository Pattern for persistence within each module.
- Use a CQRS-like approach where modules communicate via events, naturally separating command-like behavior (publishing events) from query-like behavior (subscribing to them).
Building this system will solidify your understanding of how these patterns fit together to create a large-scale, maintainable, and evolvable software system—the ultimate goal of a software architect.
Summary
- Project 1: SOLID Refactoring Challenge: C#
- Project 2: Simple Layered CRUD API: C#
- Project 3: Introduce the Repository Pattern: C#
- Project 4: Hexagonal Architecture To-Do List API: Java
- Project 5: DDD-Lite - A Simple E-commerce Shopping Cart: C#
- Project 6: Full DDD Application with Persistence: C#
- Project 7: CQRS Blog Platform (Command & Query): Java
- Project 8: Event Sourcing: Bank Account Ledger: C#
- Project 9: Build a Tiny Message Broker/Event Bus: Go
- Project 10: Modular Monolith for a Booking System: C#
- Project 11: Create a Pluggable Data Processing Pipeline: Java