Business Logic vs Everything Else

Ray Epps
7 min readSep 2, 2022

--

No matter the industry, language, or stack every line of your API is one of three types.

TLDR;

If you’re writing ‘best practices’ applications using ‘modern design principles’ your code is probably a mess because you’re following a framework or pattern without thinking critically about what your code is made up of.

A System of Identification & Organization

You Need a System

Before you can accurately implement a system of organization you first need the ability to identify what the elements you have to work with are. If you don’t know what you’re working with, how can you design a system around it?

Everything with a place, everything in it’s place

If a thing does not have an assigned place, how will it ever be put ‘away’ in an assigned place? My great uncle told me this in regards to lost toys when I was young. Lucky for us, it applies to life generally. A thing without a dedicated place of belonging can never be put in the correct place — it does not have one.

You’re reading this because you’re a nerd so with respect to software, what are the things in your application? Maybe some business logic? Have you identified it? Have you logically assigned it a place?

In my experience, you probably pulled a framework off the shelf and just write the code where they tell you to. Billion-dollar businesses have been built this way, but very few. Let’s do better.

The Types of Logic

It can be hard to distinguish one type of logic from another. We need to know what types of logic exist — in our code — and how to identify them. Otherwise, we can’t organize our application into a system correctly.

This is made worse when, having not been taught any better, we assume that there could be an unlimited number of unknown types, groups, and families of logic. This was me but it’s not true.

In any API there are only three types of logic: business, access, and utility.

Access Logic

Every external resource (i.e. anything that requires IO/async interaction — including the local filesystem) should have a module that owns its interface. This means that all interaction with an external resource occurs through a single module that has the responsibility of communicating with the resource.

The iconic example is the database. You should have a module that knows how to interact with the database and exposes functions that accept input without requiring any database knowledge.

A few examples of the wrong abstraction:

  • Your database.js module exposes a function with the signature query(sql: string) => Row[]. This will cause higher level modules to know SQL, to write SQL, and to pass SQL down to the database module. Also, because the function returns the Row type (presumably a database row) modules have to know how data is stored so they can correctly parse it into a model type. We often store data in the database in obscure formats that are more preferrable to the database we’re using or more performant for our access patterns. Only one module should be responsible for knowing about that format, mapping to it, and mapping from it.
  • You need to store files in AWS S3 so you make an s3.js module that exposes a function with the signature listInBucket(bucket: string) => S3Object . We can tell this is wrong because bucket and S3Object are both AWS S3 constructs. Higher level modules will have to know about buckets to pass the correct value. They also need to know what an S3Object is to parse the result into a file path or data stream they can use.
  • You need to make http requests to a third party API, let’s say it’s the Abstract API, so you install superagent and make the calls wherever you need to. In your controller, in your endpoint, in your validation… anywhere.

Now, some correct abstractions:

  • Your database.js module exposes a function with the signature listAllUsers() => User[] . As a new developer if I need to list all the users I can find the function and call it without needing to know anything about the database. A better design like this typically means a larger surface area on the exposed interface. Instead of exporting a single function query we’ll probably export dozens that are all specific to a model and use case. Internally, this database module will do mapping from the User model type to the type stored in the database.
  • You rename your s3.js module to file-storage.js and export a function with the signature listInDirectory(directory) => File[] . Everybody, even the self taught engineers and those who went to a boot camp [caml down, I’m calling out myself] can understand this interface. Inside the module, we’ll map directory and file constructs to buckets and S3Objects.
  • Abstract is a great example because they provide a dozen+ APIs. If your using their ip-geolocation API then you would make a module called geo.js and export a function with the signature lookupLocation(ip: string) => Coordinates . Without knowing anything about Abstract, anyone can see a function that takes an IP and returns map coordinates.

As a rule, if a module does not own the interface to an external resource it should be deterministic. This means any action should produce the same result given the same inputs. This is inherently not the case with a database where the same call executed twice could return different results (i.e. getting a user could return different results if the user just updated their profile).

These other modules are utility logic.

Utility Logic

If you have a file or folder called helpers, utility, shared, or common you are the problem and I am not speaking to you. Utility logic is not the catch all case for anything that you couldn’t find a good home for otherwise. Utility logic is the logic in any API written in any language and on any framework that is all the same.

If you’ve been working on backend services for long you’ve probably written query validation a few dozen times. Even if you used a library and let it do the work, you wired it up. Regardless of what your CEO says, that is not intellectual property. Nobody gives a sh** how you validate the query parameters sent to your API.

Routing, query param validation, json body validation, CORS, or anything else that has zero affiliation with your product, business, or idea— and does not own an external interface — is utility logic. If written well, the utility logic is the stuff you don’t have to change when you pivot to crypto.

You might be looking at your code and see that this type of utility logic has been wrapped up with all sorts of other logic and therefore is intellectual property… that’s bad, keep reading.

Based on the idea that this code doesn’t change between APIs you might think this includes modules like the database. Keep in mind that a good abstraction in the access logic surfaces functions that take in and produce model types. If you’re building a real estate platform, your database module takes in and returns models like House Property Buyer and Seller. This will change when you pivot to crypto so it is not utility logic.

Business Logic

Ah, the holy grail. It gives value to the business and purpose to the application. Simply put, it is the decision-making logic. The job of utility logic and access logic is to do what it’s told — by the business logic. Because of this, I often call it orchestration logic.

The utility logic handles a request, doing the typical work applicable to any API to prepare the request for handling, then passes the request to the business logic. The business logic orchestrates one or more access logic modules to get the data it needs to make a decision and use the access logic to persist or record that decision.

Business logic is all that really matters in a codebase. You may have 1,000,000 lines of code but only the 1,000 lines of business logic actually matter. Everything else only exists to support the execution of the business logic.

Business logic is valuable but also costly to maintain so we want as little of it as possible. We want dense business logic with high volume value and low volume code.

The Right Abstractions

The ability to distinguish one line of code from another is only helpful if you’re willing to use it to make the right abstractions. Depending on your chosen framework and patterns the abstractions can look very different. Here are some guidelines to use now that you can identify what’s what.

  1. Being the only code that really matters in your application, the business logic should stand out at the highest level. When someone reads your code how many files, modules, classes, and lines of code do they need to read through before they hit the business logic? Good abstractions around business logic bring it to the surface and push the utility and access logic below.
  2. Do not let access logic modules depend on each other. This is an anti-pattern I see often embraced as a feature and even mixed with business logic (I’m talking to you NestJS). If you need a dependency injection framework you’ve made a mistake. If the file-storage module needs the database module, stop to ask yourself why? Any case like this should be orchestrated with business logic. The business logic should know about both modules and call both of them, making decisions about how, why, and when.
  3. Whenever possible, write utility logic in a way that abstracts the framework. Do not write a query validation function that accepts an AWS Lambda Context object as an argument. Similarly, do not write a JSON validation module that accepts the Express.js Request object as an argument. Utility logic is utility logic because of the fact it is applicable everywhere. Don’t limit yourself by writing it tightly coupled to a single framework.

The Moral

It’s possible you have a way you prefer to organize and abstract your modules. No matter how you use a pattern of abstraction like I’ve described here, or a different pattern that’s obviously incorrect, you can’t concretely apply any organization or abstraction until you can distinguish blocks and lines of code from one another.

I’m Ray, I created Radash, the ACP design pattern, and the Exobase library. I don’t want your money, I just want to work on codebases that don’t suck so please follow, comment, interact, and let’s make better practices together.

--

--