Someone said the dreaded architecture word and youâre Not-An-Architect?
Scared when someone challenges your understanding of computational complexity when all youâre trying to do is put a widget on a webpage?
Never fear â itâs probably not nearly as sophisticated or as complex as you might think.
Architecture has a reputation for being unapproachable, gate-kept, and âhard computer scienceâ. Most of the software architecture you run into, for average-to-web-scale web apps, is astonishingly similar.
Weâre going to cover the basics, some jargon, and some architectural patterns youâll probably see everywhere in this brief architectural primer for not-architects and web programmers.
What even is a webserver?
Ok so letâs start with the basics. The âwebâ, or the âworld wide webâ â to use its hilariously antiquated full honorific, is just a load of computers connected to the internet. The web is a series of conventions that describe how âresourcesâ (read: web pages) can be retrieved from these connected computers.
Long story short, âweb serversâ, implement the âHTTP protocolâ â a series of commands you can send to remote computers â that let you say âhey, computer, send me that documentâ. If this sounds familiar, itâs because thatâs how your web browsers work.
When you type www.my-awesome-website.com
into your browser, the code running on your computer crafts a âhttp requestâ and sends it to the web server associated with the URL (read: the website address) you typed into the address bar.
So, the web server - the program running on the remote computer, connected to the internet, thatâs listening for requests and returning data when it receives them. The fact this works at all is a small miracle and is built on top of DNS (the thing that turns my-awesome-website.com into an IP address), and a lot of networking, routing and switching. You probably donât need to know too much about any of that in real terms unless youâre going deep.
There are tonne of general purpose web servers out there â but realistically, youâll probably just see a mixture of Apache, NGINX and Microsoft IIS, along with some development stack specific web servers (Node.js serves itself, as can things like ASP.NET CORE for C#, and HTTP4K for Kotlin).
How does HTTP work? And is that architecture?
If youâve done any web programming at all, youâll likely be at least a little familiar with HTTP.
It stands for âThe Hyper Text Transfer Protocolâ, and itâs what your browser talks when it talks to web servers. Letâs look at a simple raw HTTP ârequest messageâ:
GET http://www.davidwhitney.co.uk/ HTTP/1.1
Host: www.davidwhitney.co.uk
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64âŚ
Accept: text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
The basics of HTTP are easy to grasp â thereâs a mandatory ârequest lineâ â thatâs the first bit with a verb (one of GET, POST, PUT and HEAD most frequently), the URL (the web address) and the protocol version (HTTP/1.1).
Thereâs then a bunch of optional request header fields â thatâs all the other stuff â think of this as extra information youâre handing to the webserver about you. After your headers, thereâs a blank line, and an optional body.
Thatâs HTTP/1.1. Weâre done here. The server will respond in similar form
HTTP/1.1 200 OK
Cache-Control: public,max-age=1
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Server: Kestrel
X-Powered-By: ASP.NET
Date: Wed, 11 Dec 2019 21:52:23 GMT
Content-Length: 8479
<!DOCTYPE html>
<html lang="en">...
The first line being a status code, followed by headers and a response body. Thatâs it. The web server, based on the content of a request, can send you anything it likes, and the software thatâs making the request must be able to interpret the response. Thereâs a lot of nuance in asking for the right thing, and responding appropriately, but the basics are the same.
The web is an implementation of the design pattern REST â which stands for âRepresentational State Transferâ. Youâll hear people talk about REST a lot â it was originally defined by Roy Fielding in his PhD dissertation, but more importantly was a description of the way HTTP/1.0 worked at the time, and was documented at the same time Fielding was working on HTTP/1.1.
So the web is RESTful by default. REST describes the way HTTP works.
The short version? Uniquely addressable URIs (web addresses) that return a representation of some state held on a machine somewhere (the web pages, documents, images, et al).
Depending on what the client asks for, the representation of that state could vary.
So thatâs HTTP, and REST, and an architectural style all in one.
What does the architecture of a web application look like?
You can write good software following plenty of different architectural patterns, but most people stick to a handful of common patterns.
âThe MVC Appâ
MVC â model view controller â is a simple design pattern that decouples the processing logic of an application and the presentation of it. MVC was really catapulted into the spotlight by the success of Ruby on Rails (though the pattern was a couple of decades older) and when most people say âMVCâ theyâre really describing âRails-styleâ MVC apps where your code is organised into a few different directories
/controllers
/models
/views
Rails popularised the use of âconvention over configurationâ to wire all this stuff together, along with the idea of âroutingâ and sensible defaults. This was cloned by ASP.NET MVC almost wholesale, and pretty much every other MVC framework since.
As a broad generalisation, by default, if you have a URL that looks something like
http://www.mycoolsite.com/Home/Index
An MVC framework, using its âroutesâ â the rules that define where things are looked up - would try and find a âHomeControllerâ file or module (depending on your programming language) inside the controllers directory. A function called âIndexâ would probably exist. That function would return a model â some data â that is rendered by a âviewâ â a HTML template from the views folder.
All the different frameworks do this slightly differently, but the core idea stays the same â features grouped together by controllers, with functions for returning pages of data and handling input from the web.
âThe Single Page App with an API
SPAs are incredibly common, popularised by client-side web frameworks like Angular, React and Vue.js. The only real difference here is weâre taking our MVC app and shifting most of the work it does to the client side.
There are a couple of flavours here â thereâs client side MVC, thereâs MVVM (model-view-view-model), and thereâs (FRP) functional reactive programming. The differences might seem quite subtle at first.
Angular is a client side MVC framework â following the âmodels, views and controllersâ pattern â except now itâs running inside the users web browser.
React â an implementation of functional reactive programming â itâs a little more flexible but is more concerned with state change events in data â often using some event store like Redux for its data.
MVVM is equally common in single page apps where thereâs two way bindings between something that provides data (the model) and the UI (which the view model serves).
Underneath all these client heavy JavaScript frameworks, is generally an API that looks nearly indistinguishable from âthe MVC appâ, but instead of returning pre-rendered pages, returns the data that the client âbindsâ itâs UI to.
âStatic Sites Hosted on a CDN or other dumb serverâ
Perhaps the outlier of the set â thereâs been a resurgence of static websites in the 20-teens. See, scaling websites for high traffic is hard when you keep running code on your computers.
We spent years building relatively complicated and poorly performing content management systems (like WordPress), that cost a lot of money and hardware to scale.
As a reaction, moving the rendering of content to a âdevelopment timeâ exercise has distinct cost and scalability benefits. If thereâs no code running, it canât crash!
So static site generators became increasingly popular â normally allowing you to use your normal front-end web dev stack, but then generating all the files using a build tool to bundle and distribute to dumb web servers or CDNs. See tools like â Gatsby, Hugo, Jekyll, Wyam.
âSomething elseâ
There are other architypes that web apps follow â thereâs a slowly rising trend in transpiled frameworks (Blazor for C# in WebAssembly, and Kotlinâs javascript compile targets) â but with the vast popularity of the dominant javascript frameworks of the day, they all try to play along nicely.
Why would I choose one over the other?
Tricky question. Honestly for the most part itâs a matter of taste, and theyâre all perfectly appropriate ways to build web applications.
Server-rendered MVC apps are good for low-interactivity websites. Even though high fidelity frontend is a growing trend, thereâs a huge category of websites that are just that â web sites, not web applications â and the complexity cost of a large toolchain is often not worth the investment.
Anything that requires high fidelity UX, almost by default now, is probably a React, Angular or Vue app. The programming models work well for responsive user experiences, and if you donât use them, youâll mostly end up reinventing them yourself.
Static sites? Great for blogs, marketing microsites, content management systems, anything where the actual content is the most valuable interaction. They scale well, basically cannot crash, and are cheap to run.
HTTP APIs, Rest, GraphQL, Backend-for-Frontends
Youâre absolutely going to end up interacting with APIs, and while thereâs a lot of terms that get thrown around to make this stuff sound complicated, but the core is simple.
Most APIs you use, or build will be âREST-ishâ.
Youâll be issuing the same kind of âHTTP requestsâ that your browsers do, mostly returning JSON responses (though sometimes XML). Itâs safe to describe most of these APIs as JSON-RPC or XML-RPC.
Back at the turn of the millennium there was a push for standardisation of âSOAPâ (simple object access protocol) APIs, and while that came with a lot of good stuff, people found the XML cumbersome to read and they diminished in popularity.
Ironically, lots of the stuff that was solved in SOAP (consistent message envelope formats, security considerations, schema verification) has subsequently had to be âre-solvedâ on top of JSON using emerging open-ish standards like Swagger (now OpenAPI) and JSON:API.
Weâre good at re-inventing the things we already had on the web.
So, what makes a REST API a REST API, and not JSON-RPC?
Iâm glad you didnât ask.
REST at its core, is about modelling operations that can happen to resources over HTTP. Thereâs a great book by Jim Webber called Rest in Practice if you want a deep dive into why REST is a good architectural style (and it is, a lot of the modern naysaying about REST is relatively uninformed and not too dissimilar to the treatment SOAP had before it).
People really care about what is and isnât REST, and youâll possibly upset people who really care about REST, by describing JSON-RPC as REST. JSON-RPC is âlevel 0â of the Richardson Maturity Model â a model that describes the qualities of a REST design. Donât worry too much about it, because you can build RESTish, sane, decent JSON-RPC by doing a few things.
First, you need to use HTTP VERBs correctly, GET for fetching (and never with side effects), POST for âdoing operationsâ, PUT for âcreating stuff where the state is controlled by the clientâ.
After that, make sure you organise your APIs into logical âresourcesâ â your core domain concepts âcustomerâ, âproductâ, âcatalogueâ etc.
Finally, use correct HTTP response codes for interactions with your API.
You might not be using âhypermedia as the engine of application stateâ, but youâll probably do well enough that nobody will come for your blood.
Youâll also get a lot of the benefits of a fully RESTful API by doing just enough â resources will be navigable over HTTP, your documents will be cachable, your API will work in most common tools. Use a swagger or OpenAPI library to generate a schema and youâre pretty much doing what most people are doing.
But I read on hackernews that REST sux and GraphQL is the way to go?
Yeah, we all read that post too.
GraphQL is confusingly, a Query Language, a standard for HTTP APIs and a Schema tool all at once. With the proliferation of client-side-heavy web apps, GraphQL has gained popularity by effectively pushing the definition of what data should be returned to the client, into the client code itself.
Itâs not the first time these kinds of âquery from the front endâ style approaches have been suggested, and likely wonât be the last. What sets GraphQL apart a little from previous approaches (notably Microsoftsâ OData) is the idea that Types and Queries are implemented with Resolver code on the server side, rather than just mapping directly to some SQL storage.
This is useful for a couple of reasons â it means that GraphQL can be a single API over a bunch of disparate APIs in your domain, it solves the âover fetchingâ problem thatâs quite common in REST APIs by allowing the client to specify a subset of the data theyâre trying to return, and it also acts as an anti-corruption layer of sorts, preventing unbounded access to underlying storage.
GraphQL is also designed to be the single point of connection that your web or mobile app talks to, which is really useful for optimising performance â simply, itâs quicker for one API over the wire to call downstream APIs with lower latency, than your mobile app calling (at high latency) all the internal APIs itself.
GraphQL really is just a smart and effective way to schema your APIs, and provide a BFF â thatâs backend for frontend, not a best friend forever â thatâs quick to change.
BFF? What on earth is a BFF?
Imagine this problem â youâre working for MEGACORP where there are a hundred teams, or squads (you donât remember, they rename the nomenclature every other week) â each responsible for a set of microservices.
Youâre a web programmer trying to just get some work done, and a new feature has just launched.
You read the docs.
The docs describe how you have to orchestrate calls between several APIs, all requiring OAuth tokens, and claims, and eventually, youâll have your shiny new feature.
So you write the API calls, and you realise that the time it takes to keep sending data to and from the client, let alone the security risks of having to check that all the data is safe for transit, slows you down to a halt.
This is why you need a best friend forever.
Sorry, a backend for front-end.
A BFF is an API that serves one, and specifically only one application. It translates an internal domain (MEGACORPS BUSINESS), into the internal language of the application it serves. It takes care of things like authentication, rate limiting, stuff you donât want to do more than once. It reduces needless roundtrips to the server, and it translates data to be more suitable for its target application.
Think of it as an API, just for your app, that you control.
And tools like GraphQL, and OData are excellent for BFFs. GraphQL gels especially well with modern JavaScript driven front ends, with excellent tools like Apollo and Apollo-Server that help optimise these calls by batching requests.
Itâs also pretty front-end-dev friendly â queries and schemas strongly resemble json, and it keeps your stack âjavascript all the way downâ without being beholden to some distant backend team.
Other things you might see and why
So now we understand our web servers, web apps, and our APIs, thereâs surely more to modern web programming than that? Here are the things youâll probably run into the most often.
Load Balancing
If youâre lucky enough to have traffic to your site, but unlucky enough to not be using a Platform-as-a-Service provider (more on that later), youâre going to run into a load balancer at some point. Donât panic. Load balancers talk an archaic language, are often operated by grumpy sysops, or are just running copies of NGINX.
All a load balancer does, is accept HTTP requests for your application (or from it), pick a server that isnât very busy, and forward the request.
You can make Load balancers do all sorts of insane things that you probably shouldnât use load balancers for. People will still try.
You might see load balancers load balancing a particularly âhot pathâ in your software onto a dedicated pool of hardware to try keep it safe or isolate it from failure. You might also see load balancers used to take care of SSL certificates for you â this is called SSL Termination.
Distributed caching
If one computer can store some data in memory, then lots of computers can store⌠well, a lot more data!
Distributed caching was pioneered by âMemcachedâ â originally written to scale the blogging platform Livejournal in 2003. At the time, Memcached helped Livejournal share cached copies of all the latest entries, across a relatively small number of servers, vastly reducing database server load on the same hardware.
Memory caches are used to store the result of something that is âheavyâ to calculate, takes time, or just needs to be consistent across all the different computers running your server software. In exchange for a little bit of network latency, it makes the total amount of memory available to your application the sum of all the memory available across all your servers.
Distributed caching is also really useful for preventing âcache stampedesâ â when a non-distributed cache fails, and cached data would be recalculated by all clients, but by sharing their memory, the odds of a full cache failure is reduced significantly, and even when it happens, only some data will be re-calculated.
Distributed caches are everywhere, and all the major hosting providers tend to support memcached or redis compatible (read: you can use memcached client libraries to access them) managed caches.
Understanding how a distributed cache works is remarkably simple â when an item is added, the key (the thing you use to retrieve that item) that is generated includes the address or name of the computer thatâs storing that data in the cache. Generating keys on any of the computers that are part of the distributed cache cluster will result in the same key.
This means that when the client libraries that interact with the cache are used, they understand which computer they must call to retrieve the data.
Breaking up large pools of shared memory like this is smart, because it makes looking things up exceptionally fast â no one computer needs to scan huge amounts of memory to retrieve an item.
Content Delivery Networks (CDNs)
CDNs are web servers run by other people, all over the world. You upload your data to them, and they will replicate your data across all of their âedgesâ (a silly term that just means âto all the servers all over the world that they runâ) so that when someone requests your content, the DNS response will return a server thatâs close to them, and the time it takes them to fetch that content will be much quicker.
The mechanics of operating a CDN are vastly more complicated than using one â but theyâre a great choice if you have a lot of static assets (images!) or especially big files (videos! large binaries!). Theyâre also super useful to reduce the overall load on your servers.
Offloading to a CDN is one of the easiest ways you can get extra performance for a very minimal cost.
Letâs talk about design patterns! Thatâs real architecture
âDesign patterns are just bug fixes for your programming languagesâ
People will talk about design patterns as if theyâre some holy grail â but all a design pattern is, is the answer to a problem that people solve so often, thereâs an accepted way to solve it. If our languages, tools or frameworks were better, they would probably do the job for us (and in fact, newer language features and tools often obsolete design patterns over time).
Letâs do a quick run through of some very common ones:
- MVC â âSplit up your data model, UI code, and business logic, so they donât get confusedâ
- ORM â âObject-Relational mappingâ â Use a mapping library and configured rules, to manage the storage of your in-memory objects, into relational storage. Donât muddle the objects and where you save them togetherâ.
- Active Record â âAll your objects should be able to save themselves, because these are just web forms, who cares if theyâre tied to the database!â
- Repository â âAll your data access is in this class â interact with it to load things.â
- Decorator â âAdd or wrap âdecoratorsâ around an object, class or function to add common behaviour like caching, or logging without changing the original implementation.â
- Dependency Injection â âIf your class or function depends on something, itâs the responsibility of the caller (often the framework youâre using) to provide that dependencyâ
- Factory â âPut all the code you need to create one of these, in one place, and one place onlyâ
- Adapter â âUse an adapter to bridge the gap between things that wouldnât otherwise work together â translating internal data representations to external ones. Like converting a twitter response into YourSocialMediaDataStructureâ
- Command â âEach discrete action or request, is implemented in a single placeâ
- Strategy â âDefine multiple ways of doing something that can be swapped in and outâ
- Singleton â âThereâs only one of these in my entire applicationâ.
Thatâs a non-exhaustive list of some of the pattern jargon youâll hear. Thereâs nothing special about design patterns, theyâre just the 1990s version of an accepted and popular stackoverflow answer.
Microservice architectures
Microservice architectures are just the âthird waveâ of Service Oriented Design.
Where did they come from?
In the mid-90s, âCOM+â (Component Services) and SOAP were popular because they reduced the risk of deploying things, by splitting them into small components â and providing a standard and relatively simple way to talk across process boundaries. This led to the popularisation of â3-tierâ and later ân-tierâ architecture for distributed systems.
N-Tier really was a shorthand for âsplit up the data-tier, the business-logic-tier and the presentation-tierâ. This worked for some people â but suffered problems because horizontal slices through a system often require changing every âtierâ to finish a full change. This ripple effect is bad for reliability.
Product vendors then got involved, and SOAP became complicated and unfashionable, which pushed people towards the âsecond waveâ â Guerrilla SOA. Similar design, just without the high ceremony, more fully vertical slices, and less vendor middleware.
This led to the proliferation of smaller, more nimble services, around the same time as Netflix were promoting hystrix â their platform for latency and fault tolerant systems.
The third wave of SOA, branded as Microservice architectures (by James Lewis and Martin Fowler) â is very popular, but perhaps not very well understood.
What Microservices are supposed to be: Small, independently useful, independently versionable, independently shippable services that execute a specific domain function or operation.
What Microservices often are: Brittle, co-dependent, myopic services that act as data access objects over HTTP that often fail in a domino like fashion.
Good microservice design follows a few simple rules
- Be role/operation based, not data centric
- Always own your data store
- Communicate on external interfaces or messages
- What changes together, and is co-dependent, is actually the same thing
- All services are fault tolerant and survive the outages of their dependencies
Microservices that donât exhibit those qualities are likely just secret distributed monoliths. Thatâs ok, loads of people operate distributed monoliths at scale, but youâll feel the pain at some point.
Hexagonal Architectures
Now this sounds like some âReal Architecture TMâ!
Hexagonal architectures, also known as âthe ports and adaptersâ pattern â as defined by Alistair Cockburn, is one of the better pieces of âreal application architectureâ advice.
Put simply â have all your logic, business rules, domain specific stuff â exist in a form that isnât tied to your frameworks, your dependencies, your data storage, your message busses, your repositories, or your UI.
All that âoutside stuffâ, is âadaptedâ to your internal model, and injected in when required.
What does that really look like? All your logic is in files, modules or classes that are free from framework code, glue, or external data access.
Why? Well it means you can test everything in isolation, without your web framework or some broken API getting in the way. Keeping your logic clear of all these external concerns is safe way to design applications.
Thereâs a second, quite popular approach described as âTwelve Factor Appsâ â which mostly shares these same design goals, with a few more prescriptive rules thrown on top.
Scaling
Scaling is hard if you try do it yourself, so absolutely donât try do it yourself.
Use vendor provided, cloud abstractions like Google App Engine, Azure Web Apps or AWS Lambda with autoscaling support enabled if you can possibly avoid it.
Consider putting your APIs on a serverless stack. The further up the abstraction you get, the easier scaling is going to be.
Conventional wisdom says that âscaling out is the only cost-effective thingâ, but plenty of successful companies managed to scale up with a handful of large machines or VMs. Scaling out gives you other benefits (often geo-distribution related, or cross availability zone resilience) but donât feel bad if the only leaver you have is the one labelled âmore powerâ.
Architectural patterns for distributed systems
Building distributed systems is harder than building just one app. Nobody really talks about that much, but it is. Itâs much easier for something to fail when you split everything up into little pieces, but youâre less likely to go completely down if you get it right.
There are a couple of things that are always useful.
Circuit Breakers everywhere
Circuit breaking is a useful distributed system pattern where you model out-going connections as if theyâre an electrical circuit. By measuring the success of calls over any given circuit, if calls start failing, you âblow the fuseâ, queuing outbound requests rather than sending requests you know will fail.
After a little while, you let a single request flow through the circuit (the âhalf openâ state), and if it succeeds, you âcloseâ the circuit again and let all the queued requests flow through.
Circuit breakers are a phenomenal way to make sure you donât fail when you know you might, and they also protect the service that is struggling from being pummelled into oblivion by your calls.
Youâll be even more thankful for your circuit breakers when you realise you own the API youâre calling.
Idempotency and Retries
The complimentary design pattern for all your circuit breakers â you need to make sure that you wrap all outbound connections in a retry policy, and a back-off.
What does this mean? You should design your calls to be non-destructive if you double submit them (idempotency), and that if you have calls that are configured to retry on errors, that perhaps you back off a little (if not exponentially) when repeated failures occur â at the very least to give the thing youâre calling time to recover.
Bulkheads
Bulkheads are inspired by physical bulkheads in submarines. When part of a submarines hull is compromised, the bulkheads shut, preventing the rest of the sub from flooding. Itâs a pretty cool analogy, and itâs all about isolation.
Reserved resources, capacity, or physical hardware can be protected for pieces of your software, so that an outage in one part of your system doesnât ripple down to another.
You can set maximum concurrency limits for certain calls in multithreaded systems, make judicious use of timeouts (better to timeout, than lock up and fall over), and even reserve hardware or capacity for important business functions (like checkout, or payment).
Event driven architectures with replay / message logs
Event / message-based architectures are frequently resilient, because by design the inbound calls made to them are not synchronous. By using events that are buffered in queues, your system can support outage, scaling up and scaling down, and rolling upgrades without any special consideration. Itâs normal mode of operation is âread from a queueâ, and this doesnât change in exceptional circumstances.
When combined with the competing consumers pattern â where multiple processors race to consume messages from a queue â itâs easy to scale out for good performance with queue and event driven architectures.
Do I need Kubernetes for that?
No. You probably donât have the same kind of scaling problems as Google do.
With the popularisation of docker and containers, thereâs a lot of hype gone into things that provide âalmost platform likeâ abstractions over Infrastructure-as-a-Service. These are all very expensive and hard work.
If you can in any way manage it, use the closest thing to a pure-managed platform as you possibly can. Azure App Services, Google App Engine and AWS Lambda will be several orders of magnitude more productive for you as a programmer. Theyâll be easier to operate in production, and more explicable and supported.
Kubernetes (often irritatingly abbreviated to k8s, along with itâs wonderful ecosystem of esoterically named additions like helm, and flux) requires a full time ops team to operate, and even in âmanaged vendor modeâ on EKS/AKS/GKS the learning curve is far steeper than the alternatives.
Heroku? App Services? App Engine? Those are things youâll be able to set up, yourself, for production, in minutes to only a few hours.
Youâll see pressure to push towards âCloud neutralâ solutions using Kubernetes in various places â but itâs snake oil. Being cloud neutral simply means you pay the cost of a cloud migration (maintaining abstractions, isolating your way from useful vendor specific features) perpetually, rather than in the (exceptionally unlikely) scenario that youâre switching cloud vendor.
The responsible use of technology includes using the thing most suited to the problem and scale you have.
Sensible Recommendations
Always do the simplest thing that can possibly work. Architecture has a cost, just like every abstraction. You need to be sure youâre getting a benefit before you dive into to some complex architectural patterns.
Most often, the best architectures are the simplest and most amenable to change.