Design - Software Engineering

System design - layers and access roles

Services running on cloud servers mostly provide programming interfaces through the hyper text transfer protocol(HTTP) and are often sizeable applications. Without a clear design they become hard to maintain over time. Moreover their features need protecting from unauthorized access. This protection can be defined in roles. I'll explore one package design and elaborate on the naming with an example service for navigating the stars.

Code examples found below you can view at gregoryv/navstart.

Domain description

Before we go into the design, let me tell you about the business of navigating through the galaxy. By describing the domain we'll be able to elicit concepts and features for our system design.

The company Future Inc. provides people means to travel the Milky Way. Customers, browse and order trips on galaxytravel.future.now. Destinations are cataloged and presented adventurously with specifications of distance, travel time, ship details as well as a captains profile.
The ships captain uses the same service to plan the entire flight. He submits a flight plan just days before departure to make sure it's as accurate as possible since space travel is not an exact science and there are lot of unknown objects about. Luckily the navigation system provides the pilots with all the information they need. Once the plan has been submitted, passengers can view route details, including interesting waypoints. Crew members also access the details of routes and possible alternatives, should there be an unforseen cosmic event.

Now that we know a bit about the domain we'll be working in, lets find the important concepts and focus on the ones part of navigating the stars.

Concept elicitation

We know the service is found at galaxytravel.future.com. This is a domain name selected because it sounds great and is easily remembered by customers when they want to elope to another part of the galaxy, imagine Luke Skywalker in a bar. It has very little to do with navigating the stars though so we should exclude that name or part of it from our design. Reason about the words in your domain before deciding on how to use them. Refactoring code is easy compared to changing peoples perception of concepts.

Several people are interacting with the service; customers, captain, crew members and passengers. Let's exclude the customer as that is a role more related to booking. This leaves us with passenger, captain and crew members. Obviously the passenger is a customer at some point but the word customer is irrelevant when it comes to navigating the stars. A passenger however, has access to viewing parts of the flight plan, which leads us to enumerating the features of our navigation service.

We recognize that the galaxytravel service, serves both customers and captains though with different purpose. In our design we'll separate these into different systems and focus on the system that provides features for maninpulating flightplans. The captain submits a flightplan whereas, other crew members and passengers can view it. Passengers can see the designated route, with details such as current location, waypoints and estimated time of arrival.

Let's summarize by grouping the concepts

Role
passenger, captain, crew member
Resource
flightplan, route, waypoint
Feature
submit flightplan, view flightplan

Note that up until now all the terminology is from the domain of navigating the stars. The only term used that somehow relates to a software is "system". Which we'll design now.

System design

The first thing we need is a name for the package or module that will contain the source code of our software.

Package naming

One way to figure out a good name is to try to write that one line package documentation sentence "Package X provides ...".

"Package galaxytravel provides applications for planning star navigation"

Sounds ok, but wait, we said that the service name galaxytravel was selected for customers and should be excluded from the navigation system. Also, as a service it provides more than just applications for planning star navigation. How about

"Package starnavigation provides means to plan galaxy flights"

Short sentence which abstracts what it provides by using the word "means" and is more specific by using "plan galaxy flights". One problem though, the name "starnavigation" is a mouthful, with five syllables. The name will be used extensively and we should try to find something shorter. Maybe

Short pronounce- able package name
"Package navstar provides a system for planning galaxy flights"

Short pronouncable name, mentions the system and its main purpose. It allows for easy discussion and ties into the domain terminology nicely. Let's stick with it for now.

Navstar implements domain logic related to planning galaxy flights. It's at the core of our design. Later we'll build other layers on top of it.

navstar Navstar is the core package with domain logic

The type system is the most prominent abstraction the navstar package provides. It's responsible for synchronizing database access and other domain related configuration. There would usually only exist one instance of the system in any running application.

navstar/system.go
package navstar

func NewSystem() *System {
	return &System{}
}

// System provides logic for reading and writing navstar related
// resources.
type System struct {
	// e.g. database, sync mutexes
}

Roles expose access to user methods. Fairly often we talk about what we can do with a system, referring to you and me as users.

- Pilots submit flightplan
- Passengers and crew member view flightplans

This translates to SubmitFlightplan is implemented by type user and accessible via the pilot role. Also ListFlightplans is implemented by type user but accessible by roles pilot, passenger and crew member.

ListFlightplans() SubmitFlightplan() setUser() navstar.Role interface User ListFlightplans() SubmitFlightplan() Use() navstar.Pilot struct User ListFlightplans() SubmitFlightplan() Use() navstar.Passenger struct User ListFlightplans() SubmitFlightplan() Use() navstar.Crew struct Different roles provide different methods

We start of by defining all roles in one file together with the interface, showing partial content below. The reason being that roles change together, ie. if we define a new feature method, all roles need updating.

navstar/role.go
type Role interface {
	SubmitFlightplan(route Flightplan) error
	ListFlightplans() ([]Flightplan, error)
	setUser(v *User)
}

// ----------------------------------------

type Pilot struct {
	*User
}

func (me *Pilot) SubmitFlightplan(v Flightplan) error {
	return me.submitFlightplan(v)
}

func (me *Pilot) ListFlightplans() ([]Flightplan, error) {
	return me.listFlightplans()
}

func (me *Pilot) setUser(v *User) {
	me.User = v
}

This design provides well defined places to implement future features. Assume the navstar system should provide planet information to users.

  1. Define resource Planet
  2. Implement feature methods on type user, e.g.
    • viewPlanet(name string)
    • savePlanet(v Planet) error
  3. Expose user methods to selected roles
Authentication is most often a service level feature.

Note that authentication is not part of this design, i.e. translating some user credentials into one specific role. The reason is that authentication is not part of the navstar domain.

At this point the navstar system is fairly well designed and we know how to extend it with new features. It's time to expose the navstar system through a HTTP programming interface.

HTTP programming interface

At this stage I haven't decided on a specific design for the interface. I know however that talking about this part of the design; we will use wording like "navstar webapi", "navstar httpapi" or even simply "navstar api". Now, a name like webapi or api alone seems a bit to generic as HTTP is not the only protocol available to us. Httpapi is a mouthful so we'll shorten it to htapi.

navstar htapi htapi package is separated from the core navstar

The htapi provides a router that exposes the navstar features using its system and roles. Resources are accessible via different URLs. The routing of a url to a specific server method is handled by the muxer. Note how in this layer we are increasingly using terms outside of the domain and more technical, which is perfectly ok.

navstar/htapi/router.go
// Package htapi exposes the navstar system via HTTP.
package htapi

import (
	"encoding/json"
	"net/http"

	"github.com/gregoryv/navstar"
)

func NewRouter(sys *navstar.System) *Router {
	mux := http.NewServeMux()
	r := Router{sys: sys, mux: mux}
	mux.HandleFunc("/flightplans", r.serveFlightplans)
	return &r
}

type Router struct {
	sys *navstar.System
	mux *http.ServeMux
}

A request from a client such as a browser would follow the below sequence.

browser htapi.Router navstar.Role Protected by role navstar.User navstar.System Unprotected GET /flightplans serveFlightplans() via muxer new: role new: user user.Use(system, role) ListFlightplans() listFlightplans() query database write http response Using navstar system via a HTTP interface

The router only propagates the request down to the muxer which is an implementation detail and can be freely replaced if needed. This way the router references everything needed by the handler functions which are bound to it.

navstar/htapi/router.go
func (me *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	me.mux.ServeHTTP(w, r)
}

func (me *Router) serveFlightplans(w http.ResponseWriter, r *http.Request) {
	var role navstar.Role = getRole(r)
	var user navstar.User
	user.Use(me.sys, role)

	plans, _ := role.ListFlightplans()
	json.NewEncoder(w).Encode(plans)
}

// getRole returns a role implementation based on the incomming
// request. This one just looks for the query parameter role. Defaults
// to the passenger role.
func getRole(r *http.Request) navstar.Role {
	switch r.URL.Query().Get("role") {
	case "pilot":
		return &navstar.Pilot{}
	case "crew":
		return &navstar.Crew{}
	default:
		return &navstar.Passenger{}
	}
}

We can keep on developing this layer until we think it's ready to let other people start using it. This would be the time you think about designing for deployment, performance and maintainers. However I won't go into those areas in this article. Let's focus on the design for one particular application we intend to deliver that can be hosted on some server in the cloud.

Application

With our domain logic in one package and the exposing http layer in another we want to provide a command that handles requests over the internet. The Go language build system defaults to folder names when building and often these command-folders are found under the folder cmd/. We can use the same method to find a good name for the package holding the application. After some interations I ended up with with the name starplan.

navstar htapi cmd starplan Command starplan exposes the htapi via a TCP server.

The reason you shouldn't name it e.g. "navstar" is that the domain of navigating stars will grow and you probably want to expose parts of it differently, thus having multiple commands.
Adding files for some of the mentioned abstractions we end up with a directory tree like this

$ tree navstar
navstar
├── cmd
│   └── starplan
│       └── main.go
├── htapi
│   └── router.go
├── package.go
├── resource.go
├── role.go
├── system.go
└── user.go

Summary

Separating the domain logic from, the application exposing it, allows your service to grow more easily. By naming components carefully we can reason about concepts such as the-galaxytravel-service, navstar-system and starplan-application, which are all easily referencable in the source code aswell. Aim to have a few, but well defined crossing points between the layers. Starplan uses htapi and navstar, whereas htapi only uses the navstar package.

navstar htapi cmd starplan internal other Dependency flow, from right to left.

For other internal domain logic that benefits from alternate naming than navstar.X, structure packages in the internal directory. A part from being hidden by the Go language it's also conceptually correct that domain internals should not be exposed to any other layer.

Try not to design all the layers simultaneously as it's easier to reason about one purpose. Start with the business domain logic and work outwards throught the layers.

See you in the stars!