summaryrefslogtreecommitdiff
path: root/_drafts
diff options
context:
space:
mode:
Diffstat (limited to '_drafts')
-rw-r--r--_drafts/program-structure-and-composability.md157
1 files changed, 157 insertions, 0 deletions
diff --git a/_drafts/program-structure-and-composability.md b/_drafts/program-structure-and-composability.md
new file mode 100644
index 0000000..3dba4fb
--- /dev/null
+++ b/_drafts/program-structure-and-composability.md
@@ -0,0 +1,157 @@
+---
+title: >-
+ Program Structure and Composability
+description: >-
+ Discussing the nature of program structure, the problems presented by
+ complex structures, and a pattern which helps in solving those problems.
+---
+
+## Part 0: Intro
+
+This post is focused on a concept I call "program structure", which I will try
+to shed some light on before moving on to discussing complex program structures,
+discussing why complex structures can be problematic to deal with, and finally
+discussing a pattern for dealing with those problems.
+
+My background is as a backend engineer working on large projects that have had
+many moving parts; most had multiple services interacting, used many different
+databases in various contexts, and faced large amounts of load from millions of
+users. Most of this post will be framed from my perspective, and present
+problems in the way I have experienced them. I believe, however, that the
+concepts and problems I discuss here are applicable to many other domains, and I
+hope those with a foot in both backend systems and a second domain can help to
+translate the ideas between the two.
+
+## Part 1: Program Structure
+
+For a long time I thought about program structure in terms of the hierarchy
+present in the filesystem. In my mind, a program's structure looked like this:
+
+```
+// The directory structure of a project called gobdns.
+src/
+ config/
+ dns/
+ http/
+ ips/
+ persist/
+ repl/
+ snapshot/
+ main.go
+```
+
+What I grew to learn was that this consolidation of "program structure" with
+"directory structure" is ultimately unhelpful. While I won't deny that every
+program has a directory structure (and if not, it ought to), this does not mean
+that the way the program looks in a filesystem in anyway corresponds to how it
+looks in our mind's eye.
+
+The most notable way to show this is to consider a library package. Here is the
+structure of a simple web-app which uses redis (my favorite database) as a
+backend:
+
+```
+src/
+ redis/
+ http/
+ main.go
+```
+
+(Note that I use go as my example language throughout this post, but none of the
+ideas I'll referring to are go specific.)
+
+If I were to ask you, based on that directory strucure, what the program does,
+in the most abstract terms, you might say something like: "The program
+establishes an http server which listens for requests, as well as a connection
+to the redis server. The program then interacts with redis in different ways,
+based on the http requests which are received on the server."
+
+And that would be a good guess. But consider another case: "The program
+establishes an http server which listens for requests, as well as connections to
+_two different_ redis servers. The program then interacts with one redis server
+or the other in different ways, based on the http requests which are received
+from the server.
+
+The directory structure could apply to either description; `redis` is just a
+library which allows for interacting with a redis server, but it doesn't specify
+_which_ server, or _how many_. And those are extremely important factors which
+are definitely reflected in our concept of the program's structure, and yet not
+in the directory structure. Even worse, thinking of structure in terms of
+directories might (and, I claim, often does) cause someone to assume that
+program only _could_ interact with one redis server, which is obviously untrue.
+
+### Global State and Microservices
+
+The directory-centric approach to structure often leads to the use of global
+singletons to manage access to external resources like RPC servers and
+databases. In the above example the `redis` library might contain code which
+looks something like:
+
+```go
+// For the non-gophers, redisConnection is variable type which has been made up
+// for this example.
+var globalConn redisConnection
+
+func Get() redisConnection {
+ if globalConn == nil {
+ globalConn = makeConnection()
+ }
+ return globalConn
+}
+```
+
+Ignoring that the above code is not thread-safe, the above pattern has some
+serious drawbacks. For starters, it does not play nicely with a microservices
+oriented system, or any other system with good separation of concerns between
+its components.
+
+I have been a part of building several large products with teams of various
+sizes. In each case we had a common library which was shared amongst all
+components of the system, and contained functionality which was desired to be
+kept the same across those components. For example, configuration was generally
+done through that library, so all components could be configured in the same
+way. Similarly, an RPC framework is usually included in the common library, so
+all components can communicate in a shared language. The common library also
+generally contains domain specific types, for example a `User` type which all
+components will need to be able to understand.
+
+Most common libraries also have parts dedicated to databases, such as the
+`redis` library example we've been using. In a medium-to-large sized system,
+with many components, there are likely to be multiple running instances of any
+database: multiple SQLs, different caches for each, different queues set up for
+different asynchronous tasks, etc... And this is good! The ideal
+compartmentalized system has components interact with each other directly, not
+via their databases, and so each component ought to, to the extent possible,
+keep its own databases to itself, with other components not touching them.
+
+The singleton pattern breaks this separation, by forcing the configuration of
+_all_ databases through the common library. If one component in the system adds
+a database instance, all other components have access to it. While this doesn't
+necessarily mean the components will _use_ it, that will only be accomplished
+through sheer discipline, which will inevitably break down once management
+decides it's crunch time.
+
+To be clear, I'm not suggesting that singletons make proper compartmentalization
+impossible, they simply add friction to it. In other words, compartmentalization
+is not the default mode of singletons.
+
+Another problem with singletons, as mentioned before, is that they don't handle
+multiple instances of the same thing very well. In order to support having
+multiple redis instances in the system, the above code would need to be modified
+to give every instance a name, and track the mapping of between that name, its
+singleton, and its configuration. For large projects the number of different
+instances can be enormous, and often the list which exists in code does not stay
+fully up-to-date.
+
+This might all sound petty, but I think it has a large impact. Ultimately, when
+a component is using a singleton which is housed in a common library, that
+component is borrowing the instance, rather than owning it. Put another way, the
+component's structure is partially held by the common library, and since all
+components are going to use the common library, all of their structures are
+incorporated together. The separation between components is less solidified, and
+systems become weaker.
+
+What I'm going to propose is an alternative way to think about program structure
+which still allows for all the useful aspects of a common library, without
+compromising on component separation, and therefore giving large teams more
+freedom to act independently of each other.