← Home

Software Architecture

One of my hobbies since the early 2000s has been thinking about (and building implementations) of different approaches for building software components for web servers.

I think I'm converging on a single use case that, when implemented, will result in the minimum API design necessary to implement functionality for all the other use cases you might want to tackle in a web/application server.

The use case is this:

  1. Create a database request pool and make a connection available for each request. Start a database transaction at the start of a request, commit all changes if the request is succesful, rollback all changes if not. Return the connection the pool after the request.

Let's write a simple script of what we want in non-object-oriented-sort-of-Python to use this in HTTP requests:

def server_http_init(server_state, handler):
    http.server_forever(handler)
       
def server_http_shutdown(server_state):
    http.shutdown()

def server_conn_init(server_state):
    for i in range 5:
        server_state['conn'].append(connect())
        server_state['num'] = i + 1
       
def server_conn_get(server_state):
    conn = server_state['conn'].pop()  # Raises an Exception if there are no left
    server_state['num'] = len(server_state['conn'])
    return conn

def server_conn_release(server_state, conn):
    server_state['conn'].append(conn)
    server_state['num'] = len(server_state['conn'])
     
def server_conn_shutdown(server_state):
    while server_state['conn']:
        conn = server_conn_get(server_state)
        disconnect(conn)

def http_request_conn_start(server_state, request, response):
    conn = server_conn_get(server_state)
    conn.begin()
    server_state['http']['request_states'][(request, response)]['conn'] = conn

def http_request_conn_end(server_state, request, response):
    if error:
        conn.rollback()
    else:
        conn.commit()
    server_conn_release(server_state, conn)
    del server_state['http']['request_states'][(request, response)]['conn']


async def handle_http_request(server_state, request, response):
    try:
        # Call each component at the start of a request
        http_request_conn_start(server_state, request, response)
        state_for_this_request = server_state['http']['request_states'][(request, response)]
        # Use the connection to process the request
        response.write(state_for_this_request['conn'].execute('SELECT page FROM t'))
    finally:
        # Call each component at the end of a request
        http_request_conn_end(server_state, request, response):

server_state = {}
try:
    # Initialise each component
    server_conn_init(server_state)
    server_http_init(server_state, handle_http_request)
finally:
    # Shutdown each component
    server_http_shutdown(server_state)
    server_conn_shutdown(server_state)

In this structure, think of anything that has methods starting server_ as being a server-level component and anything with methods starting http_request_ as being an HTTP-request level component.

This means we have two server-level components: http and conn and one HTTP-request-level component conn. As it turns out the HTTP-request-level component conn makes use of the server-level component conn.

What's really interesting about arranging things this way is that it results in the entire state of the application and all components being held in one big data structure like this:

server_state = {
  'conn': {
    'num': 1,
    'connections': [<connC>]
  },
  'http': {
    'request_states': {
      (<request1>, <response1>): {
        'conn': <connA>
      },
      (<request2>, <response2>): {
        'conn': <connB>
      }
    }
  }
}

Imagine how easy this would make most debugging!

Of course, the state of the application in this implementation is entirely explicit and available everywhere. Which is precisely why you don't ordinarily see code written this way - one component could wreak havoc with all the others since it can access/change/delete all their state too.

Once you can visualise this structure you can imagine it changing as HTTP requests come in and server-level state resources such as database connections get temporaily borrowed by those requests and then put back.

This sharing of app-level resources by request-level components in order to generate a response is really the main thing that is ever going on in most web-frameworks.

You can also imagine another component entirely unrelated to HTTP also using the server-level conn for its own tasks (e.g. a background worker process). The conn component is useful for anything that needs database connections as nicely-isolated components should be.

Keeping clear in your head what is server-level state and what is HTTP-request-level state is really important.

In the real world things often get complex. What if, for example you want a component to do some logging. Say you want your logging component to work on Google App Engine and a standalone Python server. Your first thought might be to make an server-level component that logs to stdout. But on App Engine, logging happens using a component that requires the HTTP request context, so the componnent has to have an HTTP-request level part too.

What had been described here isn't a pure middleware architecture, and it isn't a pure hexagonal architecture. It is a bit of a mix of both. Since the functionality of components is often tied to the requests that triggered them, it isn't always easy to have separate hexagonal components running separately from the HTTP server. Likewise, making everything middleware doesn't respect the fact that not everything will be triggered by HTTP.

When I look at Python WSGI Middleware, Node.js Express Middleware or Golang's Gorilla components like sessions, I just see it as a refactoring of this structure where the server-level state and the HTTP-request level state is grouped by the component rather than by the type of state it is. The same variables are there, they are just stored with the code of the component rather than with other state that changes at the same level of the application.