Improving Software design through Execution Flows

One of the important things which i have started following and has helped me a lot in achieving better designs is to develop a deep understanding about “How is the execution flow” happening.

What is an Execution Flow ???

In most simple terms, Execution Flow is how is the flow of instructions across the piece of executing code.

Classes have methods and these methods across classes interact with each other. This could either be by message passing or by function calls. So when one method M1 of a class C1 calls another method M2 of a different class C2 , we consider it as the execution flow has changed from <C1> ——>  <C2> and when this Method M2 in Class C2 returns the execution flow is back again to Method M1 in Class C1 so essentially <C2> ——>  <C1>. So this is the execution flow of a particular API. However in a feature , there might be multiple APIs and each API could essentially have a distinct execution flow.

There is a lot of information which is embedded in these execution flows which can help us improve our code design and performance in some of the cases, just the point is that we need to thoughtful enough to see the execution flow.

Suppose we have 3 classes in some functionality and they interact in this as follows:

GET /user/:user-id

  • Execution Control transfers from <Class C1> to <Class C2>
  • Then the execution control passes from <Class Ç1> calls <Class C3>
  • Then the execution control passes from <Class C1> again to <Class C2>

This is how the execution flow of this API for GET /user/:user-id looks like

Blank Diagram (9).png

How can we leverage execution flow for better design ???

Lets implement a sample program which implements this GET /user/:user-id. Brief overview of this program is as follows

  • We have a manager class which is responsible for delegating commands to the store + cache.
  • We have a store which is responsible for talking to the actual datastore or DB and get / put the actual content
  • We have a cache which is responsible for storing the entries in a in-memory cache of some type.
class UserManager(userStore: UserStore, userCache: UserCache) {
  def getUser(userId: String): User = {
    val userOpt = userCache.getUser(userId)
    userOpt match {
      case Some(user) => user
      case None => 
        val user = userStore.getUser(userId)
        userCache.putUser(user)
        user
    }
  }
}
class UserStore(userDB: Db) {
  def getUser(userId: String): User
}
class UserCache(userDB: Db) {
  def getUser(userId: String): Option[User]
  def putUser(userId: String): Option[User]
}

In this sample example , we can clearly see that the information flow is as follows

Blank Diagram (8).png

In the above example or design of the solution , some people might be able to point out that design wise the code isn’t that great and it would be much better if the same class which is handling the cache is responsible for writing the data to the cache. Reason being that with the mentioned design, one can easily handle classic issues which come with caches, those being Herd Effect and Cache Coherency in case of DB Failures.

So, let’s come to our hypothesis for information flow which would help us achieve better designs or better code structures.

Core Hypothesis !!!!!

In a given API call, ideally each of the classes should be traversed only once. Information Flow should look like a DAG traversal where each node is only visited once in an execution Unit ( API in our case ). If your information flow visits some nodes more than once , then one should try visiting the design or the code structure. This is because let’s say that Node did some transformation or added something on the coming data, then if we are applying the same transformation or adding the same stuff again in someway.

 

Blank Diagram (12).png
Each class should be traversed only once in a given API call

 

Now if we apply the above mentioned suggestions to our initial design of User Feature, we will get the following design or code structure

Blank Diagram (10).png

Now if we see these code pieces, they seem just perfect because now it seems much more clean and the responsibilities are much more clearly distinguished. By using a single store + cache implementation, we have simplified the responsibility of the API Manager and handed over the cache issues to be managed by the cache itself which seems to be the right way to do it because cache itself can manage its resources. Issues like Herd Effect and Cache Coherency could be easily solved in this design as well.

Other Aspects of Better Execution Flow

  • The above mentioned core hypothesis of a simple execution flow also helps us identify other pitfalls in our design which helps us achieve better performance in our softwares. Let us take a simple example.

GET /user/query=”place=texas and has_car = true”

class UserManager(userStorE: UserStore, carStore: Carstore) {
  
  def getUsers(place: String): Seq[User] = {
    val relevantUsers = userStore.getUsers(place)
    relevantUsers.filter(user => carStore.hasCar(user))
  }
  
}

Execution Flow between the classes UserManager, UserStore, CarStore is as follows

Blank Diagram (13).png

If we look into the execution flow, it is clearly visible we are visiting the class CAR Store more than once in an API call for GET /user/query. This essentially means we can improve upon the current design by making sure that we access the CAR Store class only a single time and this leads to a better design for this API

Blank Diagram (14).png

class UserManager(userStorE: UserStore, carStore: Carstore) {

  def getUsers(place: String): Seq[User] = {
    val relevantUsers = userStore.getUsers(place)
    carStore.getUsersWithCar(relevantUsers)
  }

}

As we can see in the above diagram, after applying our core hypothesis of a class being visited only once, we were able to improve the performance of our API.

 Important Considerations

  • Ideas we just discussed should hold true in most of the designs. But for this idea to work you must need to make sure that you have some basic sanity in class designs. For this you can try to follow one of the core principles of class designing that is Single Responsibility Principle.
  • Whenever you try to design your software, try to use the above defined execution flow along with the other software design principles like SOLID principles

References

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.