The objective of this article is to:
- Educate readers on the foundational aspects of software architecture, including the importance of early design decisions and their impact on system development.
- Introduce various documentation practices like RFCs and architecture documents, highlighting their roles in planning and feedback processes.
- Demonstrate the value of prototyping and proof of concept in addressing unknowns and refining project approaches.
- Explain the principles of domain-driven design and its benefits in managing business complexity and improving code readability and maintainability.
1. Software Architecture
1.1. The Vibe of Software Architecture
Software architecture is all about those big-brain design choices and decisions you make early on in a project. These choices are like the foundation of a house – they shape how the system is built, how easy it is to upgrade and maintain, and how quickly new engineers can get up to speed.
People often use “Software Architecture” and “Software Design” interchangeably. We prefer “Architecture” because it feels like designing a building. Think of it like this: creating a property has two main stages – drawing up the blueprints and then actually building it. These stages are connected: you can tweak the original plan as you go if the initial assumptions don’t hold up.
But here’s the twist: designing buildings and software are quite different. Building architecture is bound by the laws of physics, but software architecture? Not so much. The constraints in software are more flexible. Instead of physical laws, what really matters are the skills and dynamics of the team, and the limitations of the technologies you’re using.
There’s no one-size-fits-all approach to nailing software design. However, there are some common strategies that many companies use. In this article, we’ll dive into:
- Design Documents, RFCs, and Architecture Docs
- Prototyping and Proof of Concepts
- Domain-Driven Design
2. Design Documents, RFCs, and Architecture Docs
In the tech world, design documents, often called RFCs (Request For Comments), are a big deal during the planning phase. RFCs are all about gathering feedback to make designs better. Engineers whip up these docs for complex projects before diving into the real work. There aren’t strict rules; RFCs are there to share context, suggest approaches, discuss trade-offs, and invite feedback.
2.1. Goals of RFCs
The main goal of writing and sharing an RFC is to speed up project completion by getting early feedback. Engineers decide how RFCs fit into their workflow. Here are some common approaches:
- Prototype → RFC → Build: Ideal for projects with lots of unknowns. Prototype first, share the RFC with a partially completed plan, get feedback, then build.
- RFC → Wait for Feedback → Build: Best for projects with many dependencies. Gather feedback from all stakeholders first to speed up progress.
- RFC → Build While Awaiting Feedback: For less complex projects. Start building when the RFC is shared, incorporate feedback as it comes in.
- Build → RFC: Sometimes engineers build first and then write the RFC to document decisions. This can be awkward if the RFC is just a formality, but it’s useful for archival purposes and getting feedback later.
2.2. Benefits
Writing and sharing an RFC has major perks:
- Clarify Your Thinking: Ever started coding only to realize you’re off track? A design document forces you to explain your thinking clearly.
- Get Important Feedback, Faster: Presenting a coded solution often leads to extra work from feedback. An RFC helps minimize misunderstandings and additional work.
- Scale Your Ideas: Without a design document, other engineers need to talk to you directly to understand your approach. Writing it down lets them read and ask questions.
- Promote a Writing Culture: If teammates see value in your design docs, they’re more likely to do the same, leading to better feedback quicker.
2.3. Reviewing RFCs
Reviewing RFCs is key to getting valuable feedback. Common methods include:
- Asynchronous Feedback: Comments in the document.
- Synchronous Feedback: Organize a meeting to discuss the RFC in depth.
- Hybrid: Distribute the document for asynchronous feedback, then hold a meeting if needed.
Choose the best format based on the project’s complexity and impact. The true goal of the RFC process is to reduce the time it takes to ship a project by getting early feedback.
2.4. Architecture Docs – The Record Keepers
Architecture documents are a bit different from RFCs. They’re mainly written to record decisions made, not to gather feedback like RFCs. These docs come in a few popular formats, and each company usually has a favorite.
Popular Formats
- ADR (Architecture Design Record): This is probably the most popular format. It’s designed for use with Git as a markdown file and has a simple structure.
- C4 Model: This one’s more detailed. It involves diagramming software architecture with four levels: Context, Container, Component, and Code. It was created by Simon Brown, an independent consultant and author.
- Arc42: This approach comes with a structured template of 12 sections, including “Context and Scope,” Solution Strategy, Building Block View, and Cross-Cutting Concepts.
3. Prototyping and Proof of Concept
How do you build a complex system that works? One way is through thorough planning. But an underrated alternative is to skip the heavy planning and start by showing how it could work with a quick-and-dirty prototype.
3.1. Tackling Unknowns with Prototypes
Complex projects come with a lot of unknowns, and planning stages often involve debates about these unknowns. Building a prototype can help address some of these uncertainties and show that an approach could work well enough.
I remember working on a project to design a new complex payment system to replace two existing ones. It involved around 10 engineering teams, each planning their own approaches. For two months, hundreds of pages of RFCs were produced, but no consensus was reached. Then, we switched gears and brought one representative from each team together. This new team spent two weeks prototyping a barebones approach. No planning documents, no comments – just coding and demonstrating through the prototype. Within two weeks, we built a prototype that addressed many conflicts and unknowns. This prototype was later discarded but served as a foundation for different system ownership and communication.
3.2. Prototyping for Exploration
Many great software architects build throwaway prototypes to prove a point and showcase their ideas. It’s much more productive to reason about concrete code than abstract ideas. When there are many unknowns and moving parts, use prototyping as an exploration tool. It’s perfect for problems where there’s not enough information to make a high-confidence plan. For example, if you need to integrate with a third-party API but aren’t sure how, build a throwaway prototype that makes the API calls and suggests how it could work.
If you can’t prototype your architecture ideas, you might be too detached from development, or the idea is overly complicated. Otherwise, you should be able to demonstrate how it works with a prototype.
3.3. Build with the Intent to Throw It Away
Build throwaway proofs of concept and make it clear they’re not for production. The point of prototyping is to prove something could work and then start building it properly after validation. You’ll learn a lot by building a proof of concept to show people and have productive conversations about something concrete and specific.
You can move faster by being clear that what you build is throwaway. There’s no need for code reviews, automated test cases, or maintainable code because it’s going to be discarded. The danger with any proof of concept is that someone higher up, like a product manager, might say it looks good and want to ship it. But it’s a prototype, hacked together without production practices. Shipping it in this state would be a bad idea. Stand your ground and refuse to ship the prototype. Build a proper version from scratch instead. This shouldn’t be difficult with a working prototype.
If there’s too much pressure to ship prototypes, use the trick of building the prototype on non-production technologies. For example, if your team uses Go for the backend, write the prototype in Node.js, which is obviously only for prototyping and won’t be shipped.
To develop better architecture approaches, use prototyping to build proofs of concept as a tool. The more you do this, the more productive you’ll become and the better architecture you’ll build.
4. Domain-Driven Design
Domain-Driven Design (DDD) starts by creating a business domain model to understand how the business works. For example, when building a payment system, you first need to understand the payment domain, business rules, and context.
4.1. Key Components of DDD
- Standard Vocabulary: The first step is to ensure everyone involved speaks the same language, called the ubiquitous language in DDD. Sit down with business domain experts to define terms and jargon. This might seem simple, but software engineers and payment compliance experts might define things differently, even for something as clear as “making a payment.”
- Context: Break up complex areas into smaller parts, known as bounded contexts. Each context has its own standard vocabulary. For example, in a payment system, contexts could be Onboarding, Paying In, Paying Out, and Settlement. Each context can be modelled independently and broken down further.
- Entities: An entity is defined primarily by its identity and has a lifespan. Many named things like parts of a system, people, and locations tend to be entities. For example, an accounting entry would be an entity in a payment system.
- Value Objects: These describe entities that are immutable, meaning they don’t change. For example, the currency of an accounting entry is a value object.
- Aggregates: These are clusters of entities treated as a single unit.
- Domain Events: These are events that other parts of the domain should be aware of and may react to. Domain events make triggers and reactions more explicit. For example, when a payment comes into a payment system, the account balance increases by the sum of the payment. Introducing a domain event like a PaymentMadeEvent makes this logic explicit; the account now reacts to PaymentMadeEvent instead of monitoring payment objects.
4.2. Benefits of DDD
- Fewer Misunderstandings: DDD reduces misunderstandings between engineering and business. Many software projects are late because engineers build something different from what the business expects. With DDD, there’s plenty of communication with business stakeholders from the start, reducing misunderstandings.
- Better Handle on Business Complexity: Business rules can be surprisingly complex. DDD helps capture and tame this complexity using bounded contexts.
- More Readable Code: Thanks to a well-defined shared vocabulary, the code is clearer. Class and variable names are more consistent and easier to understand, making the code cleaner overall.
- Better Maintainability: Easier-to-understand code and a defined vocabulary improve maintainability.
- Easier to Extend and Scale: When new business use cases need to be added, they can first be inserted into the existing domain model. Once the logical extension is made, implementing the change at the code level is easier. Scaling the codebase by adding many new businesses use cases becomes much easier and less messy.
5. Conclusion
In conclusion, mastering software architecture is crucial for building effective and efficient systems. By understanding and applying design principles, leveraging documentation practices, and utilizing prototyping and domain-driven design, developers can significantly enhance their ability to create high-quality software. This article has provided insights into these key areas, offering practical guidance for both seasoned professionals and newcomers to the field. Embracing these practices will not only streamline development processes but also foster better communication and collaboration within teams, ultimately leading to more successful projects.