Application Entry Points

May 22, 2024

An application entry point are the functions in your system that allows your application to take input from the user to do something with. By convention, most programming languages have a main function that does this for us. However, depending on what we are doing, the main function may not really be all that important. As an example, in web development we do not really care about our programs main function as it is usually abstracted away from us. We would generally have a controller, web socket handler, queues, etc. The important thing to know about these is that they all act as an entry point into our actual system.

When using the phx_new generator, a brand new Phoenix project will generate various pieces for you. But the parts that I want to focus on are what is inside the lib/ directory. Phoenix will generate you

  • lib/my_app/; and
  • lib/my_app_web/

The idea here is that you put all of your business logic inside of lib/my_app/ and all of your web specific logic goes into lib/my_app_web/. However, something that I have seen organizations do over and over again is not add any additional directories to this. They will of course add things like lib/my_app/accounts/ or whatever other contexts they want to split their business logic into. But what we need to get used to is adding additional “top-level” directories to our applications. If you have the need to bring a queue into your system such as RabbitMQ or Kafka, it would be best to separate that into its own directory such as lib/my_app_queue/ or similar. The reason for this is that it is a completely different entry point into your system. A queue should not care about things like HTTP status codes or any other part of the HTTP stack (unless it is using HTTP in order to retrieve items from the queue) and should be treated as something different within your system.

It is also a good idea to have your entry point functions be as small as possible. They really only need to care about taking the data from whatever source (controller, web socket handler, queue, etc) and parsing that into something that your application can actually do something with. For example, lets say we were building a user registration system in our application that sends the user a welcome email upon successful registration. Lets take a look at two different approaches that we could take in order to accomplish this.

# Controller
def create(conn, attrs) do
  case MyApp.Accounts.register_user(attrs) do
    {:ok, user} ->
      MyApp.Notifier.send_welcome_email(user)

      ...
    {:error, changeset} ->
      ...
  end
end

Note how we are calling both Accounts.register_user/1 and Notifier.send_welcome_email/1 in the controller here. Lets take another look at how we could build this.

# Accounts
def register_user(attrs) do
  with {:ok, user} <- insert_user(attrs) do
    MyApp.Notifier.send_welcome_email(user)

    {:ok, user}
  end
end

# Controller
def create(conn, attrs) do
  case MyApp.Accounts.register_user(attrs) do
    {:ok, user} ->
      ...
    {:error, changeset} ->
      ...
  end
end

In the above case, sending the email is actually part of the Accounts.register_user/1 function. After all, whenever we register a user we want to send that email, so why would we have it outside of the function that registers a user? If we were sending emails outside of that function, it would be easy to forget to actually make that call if we had another way to register users such as server side rendered pages, an API, etc.

The above case was rather simple with only inserting a user into the database and sending an email when that was successful. But you could imagine a system that needs to do a lot more things for a given endpoint such as queuing jobs, calling third party systems like stripe, doing database operations, etc. At the end of the day, we should be making our entry point functions as simple as possible. They should be doing the minimum amount of work to take whatever data they are given and parse that into something the business logic can work with then translating that into whatever format the endpoint is expecting (HTML, JSON, ack a queue, etc). They should not have the complex logic in them that decides what pieces should and should not be included in the given workflow. It is much better to do that kind of work in the business logic.