Explicit Software Design Series

Sometimes, I read books about software design and development. Usually, they are about “enterprise software” and rarely discuss their applicability in web development. However after reading them, I’m always interested in whether the techniques from these books can be applied to my daily tasks, whether they are profitable and justified.

This series of posts is an experiment where I want to test it. I will try to design and write an application taking into account the recommendations and rules that I have read about in various resources over the past years.

Some of these principles I have already applied in production, some—only in pet projects, and some of them—not at all. I’m interested in gathering everything in one application and seeing how different buzzwords from the programming world interact with each other.

Let’s clarify from the start:

This is not a recommendation on how to write or not write code

I am not aiming to “show the correct way of coding” because everything depends on the specific project and its goals. My goal is to try to apply the principles from smart books in a frontend application to understand where the scope of their applicability ends, how useful they are, and whether they pay off.

Take the code examples in this series not as a direct development guide, but as food for thought and a source of ideas that may be useful to you.

Constraints and Limitations

We will be writing code “by the book” following the rules from different sources. This means that some solutions will be intentionally “too clean” even if it doesn’t make sense from a pragmatic standpoint. Although I will mark such places in the text, try not to forget that this series is an experiment and the code might feel unnatural.

When using different techniques, I will refer to specific authors and their works. Sometimes I will provide comments with my evaluation, but for the most part, I will try to refrain from making judgments about whether something should be used or not.

On the contrary, we will try to mix in as many ideas as possible to test them. Most likely, as a result, we will write code that will not look like “standard” React application code, but in it, we will try to find connections and parallels with modern development patterns.

About the series

In this 10-part (so far) series, we will be building a fictional currency converter application. Each post will focus on a specific topic, and the plan and content will be as follows:

In addition, I have an idea to write a few related posts on similar topics:

  • Applicability of everything described with frameworks (for example, Next.js)
  • Improving type safety with type branding (for example, for DDD)
  • Code splitting, routing, and performance (for example, using Suspense and use)
  • Functional error handling (for example, with Result<TOk, TErr>)
  • Applicability with other UI libraries (for example, Solid or Svelte)

…But for now, I’m not sure if these topics will be interesting and I don’t know how much time they will take. If you’re interested in reading about these topics as well, please, email me.

Sample Application

As an example, we will write a converter for currencies from the “Star Wars” universe:

Screenshot of the main screen of the completed application
Screenshot of the main screen of the completed application

The converter will convert Republican credits (RPC) to Imperial credits (IMC) and some other currencies from the lore of the universe. We will come up with the “exchange rates” ourselves and take them from a simple JSON server. We will probably save fresh exchange rates to local storage, and closer to the end of the experiment, we will expand the application with a new feature.

But the App is So Small!

The principles we will be discussing are indeed not necessary for a prototype or a small application. However, I chose a small application for two reasons:

  1. The smaller the application, the easier it is to focus on the technical aspects and development convenience.
  2. I am lazy and didn’t want to write a large application in vain, but didn’t have any ideas for a useful open-source application.

Perhaps the benefits would be more visible in a larger application, but I decided that a simple converter would be sufficient for the experiment. If you have any ideas on how to improve this, please email me or open an issue on GitHub. Let’s discuss it! 👋

Source Code on GitHub

We will develop the application step by step. The results of each stage of work can be found on GitHub. Each post will refer to a separate folder in the project repository, where you can see how everything is organized and play with the code.

The source code of the posts themselves is located in the repository of my blog. If you have found a typo, error, or just want to add to the text, send an issue or pull request!

Application Functionality

In the posts, we will assume that we have can talk directly to the product owner and we know what functionality we want to see at the end of the development. We will consider the following use cases:

  • Entering an RPC value that automatically converts the value to a quote currency.
  • Choosing an alternative quote currency with automatic conversion of its value relative to RPC.
  • Updating quotes based on data from the server and saving this data locally on the device.
  • Simple user action analytics, but probably not in the beginning of the development.
  • In the future, we also plan to add another feature that isn’t known for now.

Problems We Want to Avoid

In the books that we will be referring to, besides the rules and recommendations, there are also counterexamples. They describe the problems that arise if the rules are not followed. We will try to avoid all of these problems and see if we can do this by following the rules from the books.

The problems we aim to avoid are:

Technical Details Over Features

The project structure should tell about its tasks and independently show what the application contains, namely features and use cases. Technical aspects of the project should not overshadow the goals and essence of the application.

Ideally, the project should help to:

  • Simplify onboarding by guiding new developers and allowing them to dive into the code gradually.
  • Speed up the search for code that is responsible for a specific part of the application.
  • Add and remove functionality without the need to change the entire application.
  • Keep in mind only the amount of information needed to solve a specific task at a given moment.
  • Speak the same language with product owners and lose less in translation from the language of business to the language of development.

High Coupling and “Ripple Effect” of Changes

Parts of the project should be able to evolve independently, and development of different features should be realistically divided among different people or teams. Parts of the application should be removable without the fear of breaking others.

The spread of changes throughout the codebase should be limited to the specific function, module, or feature where the changes were required initially. Unrelated code should not be changed.

If requirements are driven by the business, they should take priority. For example, if a feature does not bring profits and only consumes resources, it should be easy to completely remove it to avoid maintaining it. Features should be easy to scale or even extract as separate independent services or applications.

Unclear Dependency Direction

The interaction between parts of the project should be clear and understandable. Data flow should be controlled, data transformations and stages of these transformations should be explicit.

Making changes to the code should not be scary. We should understand which parts of the code will be affected by a specific change. Low-level code should not affect the design of user scenarios or the development of the project as a whole.

Leaking Abstractions and Unclear Responsibilities

The code and dependencies involved in each specific task must be explicit. Code that is not needed in the task should not be used and run.

Third-party tools and auxiliary libraries must have a clear scope and boundaries. The amount of code related to them and their influence on the project must be limited and fit in the heads of developers.

The amount of implicit input data for a function or module should be minimal. The amount and size of data, resources, and dependencies that are involved should be trackable and measurable so that we can control their growth and consumption. Sudden and disproportionate “spikes” in these resources should be easily visible.

Urge to Make Decisions

A project should not require us to make “big decisions” until we have studied the domain, constraints, and business priorities.

We want to understand what tools we should integrate into the project. It should be clear which tools fit our task, what the risks of using them are, and what degree of integration will be justified. Tooling should not dictate the principles of work and create unreasonable restrictions for business tasks.

Early on, we want to avoid unnecessary generalizations. Principles and rules should be worked out evolutionarily, in a competitive environment, based on the benefits they bring.

Brittle Tests

Tests should catch regressions and not hinder development. It is desirable to avoid “brittle tests,” “test friction,” and “test-induced damage.” Tests should be resilient to refactoring and extending functionality.

There should be no code that is unclear how to test. Each part of the project should solve a clearly defined task, and the result should be easy and obvious to test.

Business Logic Mixed with Infrastructure Code

Infrastructure code (“glue” that holds everything together) should not intertwine with business logic (which contains ideas that bring money).

Business Logic Mixed with Cross-Cutting Concerns

Adjacent tasks like analytics, internationalization, performance profiling should also not affect business logic code unless justified by use cases.

Development Principles

All of the problems listed above can be reformulated as principles that we will adhere to during design and development:

  • The main part of the application should be the business logic.
  • The project structure should reflect the essence of the application.
  • The influence of infrastructure, UI, and adjacent tasks should be minimal.
  • Code should be easy and clear how to test.
  • Modules should be as independent as possible.
  • The code should not force making serious decisions at early stages.

We will discuss each of these principles in the next posts and test them in practice.

Next Time

Today we described the constraints, assumptions, goals, and principles that we will use going forward. In the next post, we will begin designing the application using these principles and write a domain model for the currency converter.

We’ll see how functional programming, static typing, and DDD help us with the design stage. We’ll consider how to incorporate domain constraints directly into the code and how to simplify testing of the domain model. We’ll design the data flows and transformations that participate in the application use cases and discuss how to speak with product owners in a common language.

Sources and References