Chapter 2. Synchronous Versus Asynchronous Rewrites

When it comes to building software, the term rewrite is often met with fear and opposition, and for good reason. The thought of throwing something away in a field with such a bias toward efficiency is terrifying. But, like most things in software, there is more than one way to do a rewrite.

In this chapter, we’ll explain the difference between what we call a synchronous rewrite (one that requires you to stop and rewrite until it’s done) and an asynchronous rewrite (one that can be accomplished in parallel with ongoing work). We’ll provide you with strategies for successfully carrying out both kinds of rewrites.

In short, the asynchronous rewrite is an extension of general principles of modularity, and separation of concerns between data, behavior, and presentation. Building with a modular approach means you’re able to build up more complex interfaces out of very simple, context-free, and stateless UI components.

The asynchronous rewrite method relies on separating out your presentation from behavioral logic and data. This separation of concerns is a helpful evergreen principle to follow: if layout, behavior, and data are separated into three distinct layers, you’re able to make changes to any one layer independently without needing to change everything at once to keep the application working. This means you could extend these same patterns to rewrite other parts of your application asynchronously, too.

Let’s take a look at each kind of rewrite in greater depth.

Synchronous Rewrite

Rewriting software synchronously is the simplest form of rewrite and is usually what people think of when rewriting software. This usually means starting a new product from scratch and building another product outside the existing codebase. Customers are using the existing product until the newly rewritten product is completed. Resources are shifted from the existing product to a new product either fully or partially.

Choosing where to allocate resources in the synchronous rewrite is filled with trade-offs, the most obvious being duplicated work as the existing product needs to be maintained as the new product is rebuilt. Many of these trade-offs depend on team size, team skill sets, and the scope of the project. For a small team and small scope, committing all resources might be the best decision. However, if either the team or scope is large it is very likely that partially committing resources to the new project will yield the greatest chances for success.

Large Teams

Software engineering teams don’t (usually) fail because of inexperienced programmers or language choices; they fail because of poor communication. As the size of the team (N) increases, the number of conversations that the group can have (X) increases quickly, as shown in the following equation and in Figure 2-1

x=n(n-1)2
Figure 2-1. Brooks’s law

Assuming a team where each engineer communicates with each other engineer, the number of conversations explodes when going from four to five members. In a large team, reducing the volume can only be done by reducing the number of people who communicate with each other, so there are fewer nodes that are connected.

This approach is likely to do at least as much harm as good, because doing a software rewrite requires a high volume of constant communication to stay in sync. Ensuring that team members are all working on different pieces and communicating the priority of the work to be done and when scope changes are just a few examples. Keeping team size smaller is important for a successful rewrite since it minimizes the chances of something being miscommunicated to a given team member.

Large Scope

With a fixed team size, the amount of time it takes to build something is directly dependent on the scope. In the synchronous rewrite, large scope means a long period of time will be spent building the new product. This is time that would have been spent on bug fixes and features in the existing product. Since customers are using the existing product, the fixes and new features are either delayed until the new product is shipped to customers, or engineers are distracted from the new product by supporting the existing one.

Large scope projects have a greater chance of schedule slippage due to scope creep: the requirements for a feature slowly grow over time as that feature is being developed. Scope creep often happens when a feature has a missing or ambiguous specification. The larger the project’s scope, the more opportunity there is for aspects to be poorly specified.

Scope creep can manifest itself in engineering, where false assumptions are made about what the feature set looks like. It can also stem from the product side where false assumptions are made about what the customer really wants. In either case, false assumptions are made at different stages that could be prevented with more information (communication failures strike again!).

Scope creep can be prevented by rigorously challenging assumptions. With one person defining the scope, that person may make assumptions to save time or because they missed something due to their unique perspective. With a group defining the scope, it is more likely that assumptions go unchallenged because the team got out of sync or false team harmony swayed a decision. In either case, having a person or small group defining the initial scope and then opening it up to others to challenge assumptions is highly effective in preventing scope creep. When all else fails, reduce scope!

When to Do a Synchronous Rewrite

If you must work with a large team or have a huge ambiguous scope, the synchronous rewrite is more risky as it can delay delivery of value to users for a long time until it’s completed. This is why rewrites have a nasty reputation. However, synchronous rewrites should be treated as another tool, rather than judged as being “good” or “bad.” A synchronous rewrite might be your best option, depending on the situation.

Products where users have learned very specific, complex workflows are great candidates for the synchronous rewrite. In this case, changing workflows suddenly for existing customers is risky because the workflows were complex and hard to learn, and sudden change to a complex or unintuitive system will throw many customers into confusion. Building a new version of the product with streamlined workflows will delight new customers while the existing product will continue to serve the existing customers. Going this route, don’t expect all customers to move to the new product. Customers using the old product will either migrate to the new product or slowly leave when the old product has no more use to them. Rapidly sunsetting the old product may force customers who would have slowly left back into the market a lot faster, and so carries some business risk.

The other case where a synchronous rewrite is necessary is if you don’t have a separation between your data, behavior, and presentation. If your API is echoing out HTML templates, or your behavior relies on a lot of JQuery directly accessing elements in the DOM, then a synchronous rewrite would be needed. This is because an asynchronous rewrite relies on having shared data access with a decoupled backend and frontend in order to work. The synchronous rewrite pattern will work better if your data, behavior, and presentation are tightly intertwined and need to be decoupled so that they are more changeable in the future.

Asynchronous Rewrite

If a synchronous rewrite is difficult due to team size or massive scope, there is another way: the asynchronous rewrite. This second rewrite strategy doesn’t have to block development on an existing product and can leverage large teams and massive scope. Steps are broken down into bite-sized chunks that can be done serially or in parallel, and can even be interrupted when the unexpected happens. This gives the team more control over the pace of the rewrite, rather than needing to do everything as fast as possible.

An asynchronous rewrite is a good option if your backend is decoupled from your frontend, and there is one point of shared data access that both your existing frontend and a new frontend can access. The more decoupled your data and presentation is, the more straightforward this is. If your application does direct DOM manipulation via AJAX calls, for example, and your data and presentation are tightly coupled, then you’d likely need to do a synchronous rewrite in which the primary goal is to separate out your presentation from business logic and data access.

Building Modularly

Building each chunk of work in a modular way enables teams to ship to both the existing and new product at the same time. Features that are developed to work in multiple contexts, with minimal assumptions on where data comes from, can be used almost anywhere.

Display Logic and Application State

As a general guideline, keeping display logic separate from the application state will help teams build modular components.

This example will show how to apply these principles with a React.js app, but it’s applicable to other UI frameworks too. For example, a modular asynchronous rewrite could also work at a more macro level using Ruby on Rails views, by having v1 and v2 view folders and swapping out newly rewritten v2 pages for v1 pages on a page-by-page basis using your application routing. There are ways of achieving modularity in other domains too, such as a microservices architecture on the server side.

What we’ll describe here is a granular level of UI modularity, at the scale of the atomic elements such as a search bar, that together make up a larger component like your page navigation bar.

If the team is using something like React (this is applicable to other UI frameworks, too), the principle of separation of concerns means keeping business logic and state out of the components.

Example 2-1 presents an example of a component that keeps its own state and contains its own business logic.

Example 2-1. A stateful Posts component
import React from 'react';
import fetch from 'isomorphic-fetch';

class Posts extends React.Component {
  constructor() {
    super();

    // the posts are stored as state within the component
    this.state = {
      posts: []
    };
  }

  componentDidMount() {
    fetch(`http://www.reddit.com/r/${this.props.subreddit}.json`)
      .then(res => {
        this.setState({
          posts: res.data.data.children.map(obj => obj.data)
        });
      });
  }

  render() {
    return (
        <ul>
          {this.state.posts.map(post =>
            <li key={post.id}>{post.title}</li>
          )}
        </ul>
    );
  }
}

export default Posts;

Compare that with the stateless (functional) counterpart in Example 2-2, where business logic is handled elsewhere.

Example 2-2. A stateless Posts component
export default ({ posts }) =>
  <ul>
    {posts.map(post =>
      <li key={post.id}>{post.title}</li>
    )}
  </ul>;

At first glance, the stateless version has less code and is arguably more readable. You might be asking where the state comes from—how is the context set?

This is where things get interesting from a modularity perspective. It’s now possible to render the Posts component with data from a number of different sources. Data could come from plain JavaScript objects, Redux, Mobx, Flux, or even a Backbone model. From the perspective of the component, it doesn’t really matter, so long as it has enough context to render (see Example 2-3).

Example 2-3. A Posts component rendered with JavaScript objects
import Posts from './posts';

// You could do the fetch here, or in Redux middleware or anywhere!
const posts = [{
  subreddit: 'birdswitharms',
  id: '77kisg',
  title: 'kamehameha'
}]

export default () =>
  <div>
    <Posts posts={posts} />
  </div>;

In the context of a rewrite, this means that the stateless components can utilize the existing application state. This is critical to being able to do an asynchronous rewrite, because it means new components can be rendered in both the existing and new application state, even though the logic that drives them is completely different!

Developing at a Natural Pace

In the synchronous rewrite, work is done in a binary fashion. A software developer can either work on the new product or the old product, but not both at the same time. Trying to work on both at the same time requires developers to context switch between the products. The outcome is generally much better when the team stays focused and context switching is minimized.

The binary nature of the work done in the synchronous rewrite means time is allocated towards either fixing bugs (possibly adding features) in the existing product, or bringing the new product closer to launch. This trade-off between existing application quality and how soon the new application can launch has two solutions: either cut scope, or sacrifice quality. Neither approach is ideal from the product and customer perspective.

Under most circumstances, cutting scope is the better choice. But sometimes, due to lack of influence with the product team, lack of experience, or because there are no features product leaders feel could be cut, the choice is made to sacrifice quality.

In this case, the engineering team might initially ship code at an alarmingly fast rate, but often the code is confusing and undocumented. Gradually, the pace slows down. No one notices the slow down at first, but at some point the team starts to feel slow. Maybe this is how your team feels now, and that’s why you’re reading this.

The way development pace slows in most applications undertaking a synchronous rewrite can be described with a logarithmic decay function (Figure 2-2).

The asynchronous scenario is quite different. Different forces are at play and the team has more levers to pull. Since work is broken up into smaller chunks that can be shared between the new and existing applications, it’s possible to solve problems in parallel. A bug fix to the existing product can be shipped as part of a feature that is rebuilt within the new product. It is important to note that there is a wait time for the bug fix—the buggy feature needs to be replaced with a newly rebuilt component—but this time is limited to the scope of the feature. When you contrast that with synchronous rewrite, you either have to wait for the entire application rewrite to finish or shift your team’s attention away from the rewrite to fix the bug.

Figure 2-2. Developer pace over time

Instead of a binary fix or wait, there is a spectrum of choices based on a weighting function:

  • weight = priority × diffInDays(today, due date)

This function optimizes for completing the maximum number of high-priority tasks. The team should be working on the tasks with the most weight (highest priority) first, and shift tasks if a weightier task is added. If the team is working on a medium-priority feature that is due several weeks in the future and a high-priority bug fix comes along that is due tomorrow, the team’s focus should shift to the bug fix.

Your team might have a different weighting function or set priorities more intuitively than using a formal weighting function (which is how most teams set priorities in reality). No matter how you prioritize, the asynchronous rewrite provides many more options than the synchronous rewrite for meeting your team’s priorities.

Large Teams

When doing a rewrite with a large team, the challenge is communication. This is because teams of five or more experience an exploding number of connections between each person. An asynchronous rewrite can have the same problems as the synchronous rewrite if the product is organized with one large team. However, since the asynchronous rewrite encourages modularity around features, there are natural boundaries to form smaller teams. A team can own the entire life cycle of a feature from the early planning stages all the way to maintenance mode. This pattern allows teams to work autonomously and synchronize only when features require cross team communication.

Using Conway’s law as a guide, you could break features into packages and have teams focus on owning separate packages. Packages could live in the same repository (a monorepo) or could be spread out across multiple repositories and pulled in as dependencies. Example 2-4 shows the file structure of a monorepo.

Example 2-4. A monorepo tree
MyProduct
  |_ packages/
    |_featureOne/
      |_package.json
    |_featureTwo/
      |_package.json

Within the root directory lives a folder named packages, which contains each feature as an individual package. Each package contains some metadata and lists external/internal dependencies. This file structure clearly delineates the feature boundaries. The boundaries keep features decoupled and let teams operate autonomously.

Large Scope

When a product has a large scope, this means a rich feature set and time to build each feature. As the scope increases, so does the time it takes to build the full scope of features in a product. With the synchronous rewrite, trade-offs must be made when bug fixes and new feature requests become known. They’re either done on the new product and end up in users’ hands when the new product is user ready, or they’re done in the existing product, which pushes back the delivery time on the new product. With the asynchronous rewrite, where features are built modularly, new features and fixes can be done in the new product and brought into the existing product as a dependency. Just as the product is broken down into modular features, the scope is broken down into smaller pieces as well. When a part of the scope is complete, resources can be shifted as needed—even back to the existing product under dire circumstances. Teams moving around the product as needed is a natural part of the process, rather than an obstacle.

When to Do an Asynchronous Rewrite

An asynchronous rewrite breaks up complexity into smaller units that teams can work on independently. If the product is already small and the team is small, the asynchronous rewrite might not be the best choice. A product with a small scope is unlikely to benefit from being broken up any further. The overhead of trying to break up the scope would likely just push the schedule back.

If the team is small, an asynchronous rewrite may still be a good choice if the benefits of breaking up the work outweigh the drawbacks, though the change will not have as much impact as having a team to dedicate to each unit of work. The work cannot be done in parallel because the team is too small to split up.

Here’s a matrix to reference when thinking about which type of rewrite to use:

Scope and Team Size Synchronous Asynchronous
Small Scope + Small Team X  
Small Scope + Large Team X  
Large Scope + Small Team ? ?
Large Scope + Large Team   X

At this point, you should be able to determine which sort of rewrite—synchronous or asynchronous—is best suited to your existing codebase, team size, and product roadmap. If you have a large product scope and a large team, doing a synchronous rewrite where you need to stop feature development until it is complete poses substantial business risk. The asynchronous option is a valuable strategy for you to reduce this risk. In the next chapter, we’ll take you through step-by-step instructions for a successful asynchronous rewrite.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.96.5