- What Is Software Architecture?
- How to Design a System
- Five Questions
- Seven Principles: The Overarching Concepts
- Designing for an Online Bookstore
- Designing for the Cloud
- Summary
Designing for an Online Bookstore
As a running example, let’s consider an online bookshop, where users can search for rare books, order them, make payments, and then track the order until it is delivered. It also includes returns and any after-sales services. This example will show us how to use the concepts mentioned in this chapter in real use cases.
As previously noted, a solid design process begins with an understanding of the business context. Take, for example, a bookstore, which can range from being quite simple (like a friendly neighborhood bookstore that announces new arrivals via WhatsApp) to extremely complex (like Amazon). These differences are shaped by the unique business context.
It falls to leadership to ensure the business context isn’t lost when faced with making tough choices or trade-offs at the information system or technology levels. We’ve already discussed five questions that aid in understanding the business and technical context, as well as seven principles for iteratively improving the system’s design in the realm of software architecture.
Furthermore, as we’ve previously discussed, our conversation primarily centers on the design layer of the information system.
First, let’s consider the business context. We have six months to take the product to market with an average team. Our initial goal is to establish the product in the market. We do not know how much load we can expect. However, the back-of-a-napkin calculator shows us 50–100 TPS (Transactions per Second) throughput, which means that the business will be in good shape. It is fair to assume that we can rewrite the system at that point.
A developer cannot make this decision; one of the leaders has to make it, which is an example of the fourth principle. The two unknowns are transaction processing at scale and book recommendations. We are able to differentiate the first problem because we need only 50–100 TPS. We need to start exploring the recommendations soon because this is unknown to the team.
As per the first principle, we should start by understanding the user journey. My recommendation is to start with a UX design. In my experience, writing a requirement specification does not work well because neither designers, developers, nor users can see the fine points in the design without experiencing the system. We need an iterative approach. A mocked UX lets everyone experience the system and iterate it.
As we alluded to previously, the design has many levels of recursive abstractions. A typical system would have a macro-level architecture that describes different services, data stores, and other middleware and how they relate to each other. Then, each service would have an architecture that describes different components and how they relate to each other, and each component would have an architecture on the code level and how those code segments relate to each other. This book focuses primarily on the first two levels. We discuss how to architect the overall system in Chapters 5–10 and how to architect individual services in Chapter 11.
Having narrowed down the UX, we should focus on the macro architecture. Figure 2.1 shows a typical macro architecture for the bookstore. Typical software architecture in the 2020s would use databases to store the state: a set of (stateless) services that handle business logic. Those services are used in one of three ways: a single-page application (SPA) running in the browser, a mobile app, or direct API calls. (Chapters 5–10 discuss these in more detail.)
FIGURE 2.1 A typical macro architecture for the online bookstore.
Services are loosely coupled with other services. If we use microservices concepts in our macro architecture, each service can be developed, released, and deployed independently. Identifying services given a problem is called service decomposition, which is a crucial skill of an architect. Chapter 5 discusses it in more detail under SOA.
Once we have identified services, the next step is to identify interservice interactions and to define message formats (APIs) for those interactions. At this point, our “do as little as possible” approach applies some friction.
It is hard to change the message formats or API of a widely used service later. We must spend time thinking through these interactions and develop a mature set of APIs. We can use user interactions identified in our UX design in this phase. Following the fifth principle, at this point, we should design deeply, define message formats, and think through immediate and long-term use cases. As part of thinking deeply, we should also define the database schema. When we deeply design both schemas and APIs, it clarifies most of the design. It is a good idea to take a lot of feedback and discussion about APIs and databases to ensure we get them right.
As mentioned, we implement slowly, learning and revising the design as we go on. A far-reaching API design gives us a broad and balanced understanding of the system. Public APIs need extra care, however. If well-defined message standards exist, we should adopt them as much as possible. For example, using JWT (JSON Web Tokens) tokens for authentication saves us the need to define a token format and also gives us the flexibility to change our identity server later.
Once we have a design, we should plan the implementation. As principles 2 and 3 mention, we should first identify a thin slice and get that working. This could be the ability to see a book, select it, and order it. Each iteration after that should create features to maximize the value they add. For example, iterations can add search, shopping cart, returns, recommendations, and so on to our online bookstore.
In parallel, as per principle 6, we need to start exploring hard problems such as recommendations and even scalable transaction processing. The reason is that we need time to get them right.
After identifying the abstract architecture, while implementing iterations, we should design our services. We can do this by deciding which parts to develop, which parts to reuse, and how to implement them. Here are some examples:
We can implement each service using tools like Spring Boot and MySQL. For services such as IAM and payment APIs, we can use either an off-the-shelf middleware or an SaaS (Software as a Service) solution.
We can implement fulfillment and return services using a message queue or a workflow system, due to their asynchronous and long-running nature.
The final choice needs to factor in considerations such as time to market, required performance, and the experience of the team. My recommendation is to start simple and add complexity as needed unless you have prior experience in building similar systems.
At some point in the middle of development, we should take the product to customers. This point is called minimum viable product or minimum lovable product. It can start with friendly users and expand to more and more users.
At each step, we should strive to learn. Although we have a design, we can modify it if our learning suggests changes. Note that this process continues as long as the system is live.
From the viewpoint of TOGAF’s three layers, the majority of the architecture we’ve talked about falls into the category of information systems architecture. Yet, most decisions are influenced by the business context, which expands upon TOGAF’s business architecture. We delve into technology architecture only when we discuss specific technologies, like Spring Boot or MySQL, and that’s mainly done as examples or to illustrate complexity.