Historical paradigm shifts in programming
Since its inception in the mid-20th century, the field
of software development has undergone several paradigm shifts. For example, the
introduction of structured programming concepts in early coding languages (such
as Fortran, Pascal and C) made the resulting code far more manageable than it had
been originally.
Later, the advent of object-oriented programming techniques allowed
encapsulation to be routinely applied in the next generation of languages, like
C++, Smalltalk, Eiffel, etc. Similarly, the subsequent introduction of software
design patterns added a significant level of sophistication to the practice of
both application and enterprise architecture.
The resulting paradigm shift could be likened to:
- Replacing “spaghetti” code (i.e. tangled modules with no structure or encapsulation)
- By “lasagna” code (i.e. layered modules, with data abstraction and information hiding)
As desktop apps gradually became ubiquitous, various types
of Windows forms were introduced to develop the interfaces for end users, for
the interactions with databases and so on. Then, as the Internet became more
pervasive, these were replaced by Web forms.
This became common in popular multi-purpose languages like C#
and Java, as well as in tools intended for more specialized purposes (e.g. like
Oracle Developer, for programming database applications).
A common sense
approach that led to implementing Agile development methodologies
Meantime, the advent of Agile development methodologies revolutionized the management of the software life cycle, by providing a viable alternative to the traditional “waterfall” model. Over time, the progression of methodologies looked something like this:
The Waterfall method:
- This approach involved the sequential development of large, highly coupled modules
- Thus the start of work on subsequent modules often depended on the completion of the previous ones
- This approach made use of paired programming and aggressive testing
- The idea was to ensure the implementation of smaller modules that worked reliably and were easy to test, instead of huge monolithic blocks that were difficult to maintain
- This was achieved by having programmers work in pairs, so that they could constantly compare ideas and ensure adequate test coverage
- This is the essence of the “Agile” approach, which was an outgrowth of Extreme Programming
- It involved deploying apps after relatively short “Sprints”, which each included their own analysis, design, development, testing and deployment phases
- In other words, each Sprint became a microcosm of the entire software development life cycle
- In order to ensure constant feedback from clients (and resulting adjustments as needed), the length of Sprints were typically measured in weeks
- Hence this represents a huge change from the Waterfall model, where deployments were scheduled over periods of months or even years
- Meanwhile the typical inefficient, boring and excessively long weekly status meetings were replaced by short, daily “stand up” team meetings (generally referred to as “Scrums”)
- This ensured that developers always had timely feedback (and if necessary, help) from their peers
- The three daily questions became:
i. What did you do yesterday?ii What will you do today?iii. What are your blockers (i.e. where can you use some help)?
- Where possible, subject matter experts (SMEs) were embedded into the dev teams, to ensure that the code continued to conform to what the clients wanted
- Evolving Agile methodologies have resulted in sub-specialties, as such as:
- “Lean” (i.e. minimal or just in time) development and
- “Scrum Masters” (i.e. the people who manage the Scums and Sprints)
- By and large though, a hybrid approach to software development has emerged
- Therefore, companies today will often pick and choose whatever development methodology (or combination thereof) works best for them.
Continuous integration
and automated database management leads to the evolution of Dev Ops
As a result of the evolving Agile approaches to software
development, the risky practice of infrequent monolithic deployments has been
gradually replaced by providing smaller periodic ones. Thus, highly coupled and
monolithic legacy apps were gradually either refactored or replaced, by a
series of decoupled “black box” modules, which only talked to each other via
strict APIs.
Many specialized languages also began to emerge for various
highly specific needs (e.g. SQL, PEARL, PHP, R, Python, Angular, React, etc.). Meanwhile,
various object relational data mapping (ORM) tools made database programming
much more accessible to programmers who were not familiar with longstanding SQL
coding practices. Similarly, with the introduction of concepts like
business-intelligence, data mining and Analytics, the administration of databases
started to become more of an ongoing, automated integration process.
Thus the various data types from relational database, data
warehouses, Data Lakes and so on all had to be combined and optimized. This was
necessary so that the underlying data could be effectively harnessed and explored.
Then, as dev ops came to be increasingly automated, the use of continuous
testing, integration and deployment became the norm.
This resulted in test-driven development and a greater
emphasis on quality control. Lately this approach has been tempered somewhat by
the concept of minimal viable products (MVP), which ensure that “the perfect
does not lead to the sacrifice of the good”. Nevertheless, the trend remains to
modularize software as much as possible.
Further replacing
monolithic applications by the introduction of modular “micro-services”
Another major paradigm shift is occurring, as software apps are
being split up into increasingly smaller components, through the increasing use
of micro-services, containers and orechestration. These components are designed
to provide highly specialized services, which generally communicate via RESTful
APIs or asynchronous message queues.
The advantage of breaking up an application into small,
manageable components (i.e. micro-services) is that the resulting modules can
be maintained, enhanced of replaced independently of each other. As long as the
interface “contracts” between them are respected by their APIs, the inner
workings of each micro-service can be fully encapsulated.
This result is true regardless of whether the modules
communicate via so-called “RESTful” HTTPS service calls, or via other API
paradigms, such as asynchronous message queues. The former type of service
calls are becoming more common and typically involve an HTTPS Put, Get or Pull of
some kind.
This generates a “response code”, which indicates the status
of the HTTP request, along with some data (e.g. using JSON, YML, etc.), if
applicable.
Asynchronous message queues are very useful when the
services that are being provided require a waiting period. Hence the message queue
is polled periodically to determine if the results of a given query are
available, but in the meantime the calling programs can continue to go about their
business.
This approach represents a significant step forward, since
it hugely diminishes the risk of making changes to a given service. For
example, if a given micro-service is negatively affected, then it simply goes
offline until it’s fixed. This approach was pioneered by Netflix when it first
introduced streaming video services, but it’s now common among Big Data users,
like Google, Facebook, Amazon and so on.
The ensuing need
for containers and orchestration
The resulting micro-service modules lend themselves
naturally to being “containerized”, using tools such as Docker. In particular,
this approach allows the micro-services to be easily instantiated, tested and
refactored independently … as long as the underlying communication contracts
between them (i.e. their APIs) are respected. [1]
In fact, as a result of the increasing implementation of
micro-services, the use of containers and orchestration actually become
essential (rather than optional) tools. In particular, “orchestration” refers
to the process of managing the containers automatically, via tools such as
Kubernetes. This introduces the next level of features, such as automated
scalability and “self-healing” to the containerized micro-services (more on
this later).
The key here is that micro-services involve numerous small
modules communicating with each other via RESTful or other APIs. When these
modules are implemented, they become virtually impossible to manage manually. Hence
the need to implement them via nodes, within containers, as well as the
accompanying need for orchestration of the nodes (i.e. they’re required, rather
than optional).
This means that a tradeoff results between the convenience
of being able to modify the containerized micro-service independently and the
ensuing need for some automated way to manage them.
While containers are implemented virtually, unlike the “virtual
machines” which preceded them, containers do not require their own operating
system. Hence multiple containers can
share a single OS and thus they consume very little in the way of resources. They
can be “spun up” or “spun down” (i.e. created or destroyed) effortlessly,
making them ideal for continuous integration and automated testing.
This means that continuous integration tools (e.g. Jenkins)
can be configured to automatically build, test and integrate the
containers. Databases can also be spun
up or down in this way, by implementing tools such as SQL Server Integration
Services (SSIS) via containers. For example, tools like SSIS can automate the
management of scripts that are used to deploy and maintain databases.
In the case of persistent data, the state must be
maintained, which is a problem for containers that are constantly being spun up
or down. Hence “volumes” are instantiated, which seamlessly connect to the
containers (i.e. unlike containers, the volumes tend to exist on real media,
such as disks).
This allows the data to persist, even when the containers
are regularly created and destroyed. As far as the container is concerned, the
connection to the database is essentially the same, whether it’s stored in a
container or a volume.
Implementing micro-services
via cloud-native orchestrated containers
The process of deploying applications “to the cloud” is
intended to ensure that additional resources and scalability are automatically
invoked, on demand, by the providers of the “platform as a service” (PaaS).
Tasks such as these are generally managed by Dev Ops
engineers, who are specialized in working with tools like Docker, Kubernetes,
Jenkins, SSIS, Kafka and so on. This, in
turn, frees up software developers to focus on what they do best - namely
maintaining and evolving their application code.
As alluded to earlier, implementing numerous parallel
micro-services requires orchestration, because it’s simply not realistic to
manage multiple containers manually. Thus
the orchestration tools provide the Dev Ops engineers with the ability to
precisely configure the apps that they deploy. As a result, the number of
instantiations available for a specific micro-service (and at any given time) is
adjusted automatically.
For example, if it’s determined (e.g. through metrics,
instrumentation, etc.) that N containers are needed to address the demand for a
given service, then Kubernetes will ensure that there are always N containers available.
In other words, if one or more Docker containers fail, then Kubernetes will
gradually replace them until the desired number is restored. This process is called “self-healing”. [2]
Similarly, if the demand increases or decreases, then the number of containers will automatically increase or decrease (as needed). This approach optimizes the use of resources, since the number of containers at any given time will be no more or less than what is dictated by demand.
[1]
Hence the remainder of this article will explore how the advent of
containerized micro-services requires orchestration, which results in
cloud-native apps that feature automatic scaling and provisioning.
[2] In
the remainder of the article we’ll use Docker and Kubernetes as examples of
containers and orchestration tools, respectively, for convenience. However,
these concepts generally apply equally to the similar products provided by
other vendors.