The importance of designing towards abstractions

The importance of designing towards abstractions

Understanding abstractions in software engineering will give us a huge boost in how we design and explain concepts

Introduction

Abstractions are a powerful tool that took me a while to understand. I want to share what I recognize as an abstraction and why it's important to think around them, making them a really powerful resource when designing software.

My name is Alberto Romero, I'm a software engineer with experience working in tech startups. I became a better junior as I understood I need to stay humble in order to grow, and not everything I learn becomes an absolute truth, everything is contextual, it all depends. Consider subscribing to my newsletter in this same page!

What is an Abstraction?

My definition of abstraction, in just a few words, is hiding implementation details under concepts. That's pretty much it, I could end the article here actually, but I'll go deeper in explaining a lot more about this phrase.

Let's start decomposing the phrase in small chunks (divide and conquer, oh yeah!):

  • Implementation Details are all those important pieces of software or mental schemes that most of the time refers to code, pseudocode, programming languages, frameworks or libraries. Besides, the level of abstraction of this pieces, when designing software, is not as relevant as identifying what are the concepts that compose a big piece of software. If you are designing a payment gateway, you should probably start putting together concepts like Customer, Payment, PaymentLog, Balance, and others, instead of deciding if you are gonna use lodash for parsing from camelcase to snakecase.
  • Hiding Implementation Details means that when we look at a design, we don't want to see the small pieces that are in charge of very low level functionalities, as they will be distractors for the important parts of a design, we want to hide those details under objects that describe better how they fit under a design.
  • Hiding Implementation Details Under Concepts is the ability of giving proper names to low level operations, making the whole system feel cohesive and well thought.

This can be explained a bit better with the next example: A low level detail can be how to treat money as cents instead of float numbers, we can make an abstraction called Money that will do that job for us, and with that name, we don't need to expose the low level concepts of software like parse_amount_to_cents or AmountToCentsParser.

A real life example of an abstraction: When going to a restaurant, we can order multiple products from a menu, we don't need to tell them exactly what we want in terms of "Can I have a piece of tortilla with meat and sauces", we usually abstract those combination of ingredients, and we call them by "Tacos", "Quesadillas", "Tortas" and other different recipes. Therefore, a Recipe is an abstraction of multiple ingredients combined under a concept

In software, an Abstraction serves the purpose of making a system feel well embedded and understandable. Looking from the big picture, it's easier to understand concepts than understanding low level variables, and that's exactly what we all do more frequently. When a stakeholder asks us to make a change in a system to support a new business rule, we tend to see the big picture and understand what that change will cause in the overall system, rather than just jumping into the code and writing a function that performs that new business rule.

Understanding levels of abstraction

Business Decisions

Referring to everything related to making a good product. This space is where people talks about what users needs are, what are the features customers want, figuring out how to decrease the churn of users, and other terminologies that are relevant for every part of the company (of course, to engineering)

Translating Business Rules to Use Cases (Business and Engineering)

How we understand the business is how we will start treating software. It's critical that we have the most information possible from the business to land the best possible abstractions of what the business is supposed to create.

Mapping correctly what the business needs to what the system should support is one of the most complicated arts of software development! In my opinion, that usually happens because teams do not find the right abstractions to construct their products. Domain Driven Design (DDD) actually encourages to have an Ubiquitous Language to ease communication between stakeholders, users and developers.

Low Level (Fully Software)

Implementation details are not bad at all, they are really really important at the moment of coding, most issues regarding scaling users, slow load times in the browser, and others, are normally because of implementation details in both Front end and Back End domains. However, I do think it's best to leave this details at the very end when analyzing how to add functionalities to a product. Finding the right abstractions for the right features has a bigger priority normally

Where's the example!

Glad you asked! Let's see some examples to understand a bit better the point! I wanna point out there's not a definite answer to what a good abstraction might be or not, it all depends.

When I was at Wizeline, we built a Chatbots platform that would reduce the amount of time and effort spent by our Services division to bootstrap a chatbot from scratch, it would basically automate the repetitive stuff so that they would focus on building a conversational bot, as opposed to spending time creating a Github repo, installing npm_modules, and dealing with repetitive coding.

An initial design of the system would look kind of similar to the following:

  1. We would build something called a Conversation Script that would store the conversation dialogs a bot should follow depending on what the user sends to the bot. That will be later decomposed to identify the user intentions and entities (i.e. if a user wants to book a flight, the intention would be book a flight and the entities would be the dates of the flight and the origin/destination.
  2. When a bot is configured to a specific messaging platform, let’s say Facebook Messenger, we would then be able to use an object called ConversationHandler that would handle the user message and other data, and then return what the bot would need to say as a HTTP Response (See Execution Timeline)

image.png

It all looks good, can we jump into the implementation?

Actually yes, we can! It’s more than ready to start coding, but, what are the foundations we are putting in place?

  1. If we think about ConversationHandler, what does that mean? I know, it’s obvious, it handles conversations, but what specifically handles about them? Does it track how often people message the bot? Does it handle the context of a conversation so that whenever a user replies back, we know in which part of the conversation he left? Does it handle the cache timeout of the conversation?

    Handler is an ambiguous concept, so if we decide to proceed, that Handler concept can become anything at all. That might seem like the wrong abstraction to what we are looking for (not just the wrong name).

    If we think about building a Chatbots Platform, what would we think is the main entity of the system? If you ask me, I would say a Bot is the main entity from a Chatbots Platform.

  2. Our ConversationHandler has something called dialogs which is a json structure that contains the different responses a bot should say depending on the intentions. Is Dialog the right concept we want to introduce? Should it be an object instead of a Json?

☕ Sometimes to land a right naming or abstraction, one should ask if a data structure should be an object, or if it should be a primitive type.

  1. The Conversation Handler calls an external library (npm module, python package, etc) to connect to an NLP Provider. The provider will then identify the Intentions and Entities involved in the conversation, so that the conversation handler would identify what to say next. Going back to the Implementation Details section, the way we would want to hide those details is by using an abstraction. In this case, it can be an object representing the responsibility of “Identifying Intentions and Entities”. Also, maybe we want to store Intentions and Entities in its own object, maybe called NLPResponse. It’s an option, I don’t know, but it makes more sense, it hides those details and let us see more intention about the system

Let’s try to land this feedback and see how a refined design would look like

Iterating the design

image.png

It seems like a very different design if you ask me! the only object that didn’t change is the FacebookMessengerEndpoint which handles the HTTP Call from Facebook Messenger.

First of all, take a look into the diagram. We should see more intention in this diagram than in the previous one. We have a new concept called Bot that responds to messages using an NLPEngine, and with the NLPResponse, it decides what to respond to the user, building a BotResponse which will be used by the Endpoint controller to respond to the user.

There can be other thousands of ways of making this design look good and find the right concepts to make the design make sense, this is just an example that came into my mind that I hope makes sense to all of you! Using abstractions (hiding implementation details under concepts) let us communicate better our decisions and our way of thinking inside a team.

Abstractions evolve over time

Once we figure out better the knowledge about the particular area of expertise our business is diving into, we will be able to modify, add or delete concepts. This is pretty important as sometimes we as engineers tend to design systems to be “future proof”, but in reality those practices are really harmful for the business and the team, and sometimes imply making tradeoffs in areas such as Developer Experience, Testing times, System slowness, Slow coding, and others.

⚠️ If you don’t need an abstraction, don’t put it, it will just be noise. Add it when it makes sense = when you start facing problems because of not having it

The cost of abstractions

Finding the right abstractions for a system has an under-looked big cost, but, quoting the Design Stamina Hypothesis article from Martin Fowler:

🤝 Design activities certainly do take up time and effort, but they payoff because they make it easier to evolve the software into the future. You can save short-term time by neglecting design, but this accumulates TechnicalDebt, which will slow your productivity later. Putting effort into to the design of your software improves the stamina of your project, allowing you to go faster for longer. - Martin Fowler

image.png Image taken from Martin Fowler's Post - Design Stamina Hypothesis

My biggest recommendation is that you try your best to negotiate landing good abstractions taking the cost of time at the beginning, but then prove that your design will pay-off in the mid term, by proving you can add and modify the codebase in a methodic and fast way, instead of in an reactive and slow way. And, as always, it depends.

  • If you are building a PoC, you might not want to build a complete design, you might just want to hack your stuff and then figure out what would be the best way to design.
  • If you are building a startup, worrying too much about design will just slow you down as your biggest priority should be Product Market Fit.

Common Abstractions

I want to share real quick some Abstractions I’ve been using in multiple teams to ease communication, so that whenever they are mentioned, most of the team already understands what they mean!

  • The concept of Client or Gateway refers to an object that communicates to a web service/provider, its only responsibility is to figure out how to reach that service and return a unified response of what we would expect from that provider, without propagating the implementation details from the library.

    Let’s say we are using a Payments Provider, and they manage errors and responses in a particular way. In this particular abstraction, we would avoid using the terminology from the provider, and use terms that would make sense within our systems.

  • The concept of Facade is useful when designing subsystems that are in charge of a particular area of expertise. It’s basically an interface that hides implementation details under certain methods that do not tell you how operations are done, but rather they just explain what they are meant to do. We could have a BillingFacade that manages all the billing for all users, and the methods in there could tell you what they do, without giving you a lot of context of how the operations are done, like pay_customer , charge_user , generate_invoice , etc.

  • The concept of Repository is useful for entities that are stored in a particular data storage mechanism such as a Database, Cache Database, File System, or others.

Conclusions

This is one of my favorite parts of building software, landing a good design that makes you feel you are understanding a problem and are able to explain to people how you intend to solve it.

Most of this abstractions follow some of the SOLID principles. I will be talking about them in my next posts. Thanks for reading!

Did you find this article valuable?

Support Alberto Romero by becoming a sponsor. Any amount is appreciated!