It came up in a recent code review that we had a method that did stuff, then called a method that did stuff, that then called a method. Eventually, the last method in the chain bubbled back up and out.

This is a bit of an anti-pattern in my eyes. Essentially you are coupling these methods (functions) together.

As a quick diversion, let's look at the idea of layered architecture.

   UI
   |
   v
 Domain
   |
   v
DataAccess
   |
   v
SqlServer

As we know, in this architecture we have coupled out UI indirectly to our database. We know this is a bad design, and we use Dependency Inversion to solve it.

Now we look at a code example as described above:
(don't trust this code as correct, I wrote it without a compiler and made up a bunch of method names)

public List<string> GetList(string filter, OrderBy ordering)
{
    return BuildRequest(filter, ordering);
}

private List<string> BuildRequest(string filter, OrderBy ordering)
{
    var request = new HttpRequest();
    request.Url = $"/list?filter={Escape(filter)}&order=Map(ordering)";
    request.Method = "GET";
    request.Auth = LoadAuth(User.Guest);
    
    return GetResults(request);
}

private List<string> GetResults(HttpRequest request)
{
    var client = new HttpClient();
    var response = client.MakeRequest(request);
    
    return ParseResponse(response);
}

private List<string> ParseResponse(HttpResponse response)
{
    var rawContent = response.ReadContent();
    var resultList = JsonParse<List<string>>(rawContent);
    
    return resultList;
}

So what's the problem? Well like above with our layer dependency, we have method dependency.

GetList => BuildRequest => GetResults => ParseResponse

While this might seem fine to you perhaps, it means we can't actually test any of these methods in isolation. It may not seem like it would be that useful in this contrived example, but you can imagine cases where it might.

For readability, it also makes it hard to see the structure of execution, because it is hidden in the implementation details.

In contrast, let's flatten the structure and make our methods more like pure functions.

public List<string> GetList(string filter, OrderBy ordering)
{
    var request = BuildRequest(filter, ordering);
    var response = GetResponse(request);
    var result = ParseResponse(response);
    
    return result;
}

private HttpRequest BuildRequest(string filter, OrderBy ordering)
{
    var request = new HttpRequest();
    request.Url = $"/list?filter={Escape(filter)}&order=Map(ordering)";
    request.Method = "GET";
    request.Auth = LoadAuth(User.Guest);
    
    return request;
}

private HttpResponse GetResponse(HttpRequest request)
{
    var client = new HttpClient();
    var response = client.MakeRequest(request);
    
    return response;
}

private List<string> ParseResponse(HttpResponse response)
{
    var rawContent = response.ReadContent();
    var resultList = JsonParse<List<string>>(rawContent);
    
    return resultList;
}

The only method that didn't change was the last one. But doesn't that already help readability? From the top method, we can clearly see the workflow of the public method, without diving into the implementation details of each method.

As an added benefit, some of these methods are now more generic. We could reuse the GetResponse method in future public methods without modification. We could also potentially reuse ParseResponse with Generics as a minor modification if we wanted.

Let's talk about a couple of the techniques I used above:

Don't mix level of abstraction

When you write code, try to build it with layers of abstraction in mind. You want to have a high-level part of your system. This could be a set of classes (say controllers, or CommandActions), or a layer of methods (perhaps the public ones) that are at a higher level of "what" is happening. These will call into more concretely named classes and methods that are named after the "what" and implement the "how".

Some more reading on levels of abstraction with Mark Needham's Coding: Single Level of Abstraction Principle article, Yuri Kasperovich's Single level of abstraction
and Arun Sasidharan's Object Oriented Tricks: #6 SLAP your functions.

Pure Functions

Functional programming has a concept of Pure Functions. Basically, you have two types of functions: pure and impure. Pure functions are deterministic, that for any input you will always get the exact same output. Think mathematical functions for instance. Impure functions, on the other hand, can produce a different result if called again. Usually, the impurity comes from an internal dependency that the function is calling. The other big difference is that Pure functions are side-effect free, while impure functions can cause side-effects.

There is an interesting property of transitivity about these two types of functions. Function calls an impure function, then that function is therefore impure. Impure functions can call pure functions, but pure functions cannot call pure functions, without that making them become impure. Simple. (I think?)

Why is this relevant? Well in our original example of function chaining, we only had one pure function: ParseResponse. This was the only deterministic function in our class. In our second example, the only impure function was GetResponse, which by transitivity means that GetList is also impure, but the rest are pure functions.

I don't know about you, but I find it much easier to unit test deterministic code rather than a non-deterministic code.

There are techniques such as dependency inversion that allows you to turn impure functions into pure functions, but I don't know enough to go into detail. (something, something, I/O Monad)

Conclusion

A small refactoring technique, but helpful to make your code more readable, deterministically testable and pure (not to mention easier to name).