Microservices are a relatively new concept in software architecture. This approach focuses on breaking down complex monolithic applications into small, isolated modules. Each of these modules, or microservices, performs only one specific task.
Interestingly, UNIX developers began applying similar principles when creating their operating system long ago. One of these principles states: "A program should do one thing and do it well." This principle suggests that a program should be limited to the required set of functions and do nothing more, while performing its task flawlessly. This closely resembles the concept of a microservice, which is also designed to perform only one specific task.
But are all the principles of microservices and the UNIX philosophy truly similar? Let's explore this further, starting with the more classic approach — the UNIX philosophy.
The history of UNIX began in 1969 when Ken Thompson and Dennis Ritchie started developing the operating system at Bell Labs. Ken Thompson, known as one of the creators of UNIX, made a significant contribution not only to the system itself but also to its philosophy.
In 1973, UNIX was rewritten from the B programming language to C, bringing hardware independence and various important features that we are familiar with today, such as different variable types (int
, char
, float
, etc.) and the need for their declaration (statically typed). Throughout the 1970s and 1980s, UNIX evolved, spreading through academic circles and commercial organizations, gradually forming its key principles.
UNIX became a revolutionary project that changed the approach to operating system development. Its creators aimed for simplicity and elegance in design, which is reflected in the system's philosophy. The UNIX philosophy, with its emphasis on modularity and efficiency, became the foundation for many modern software development approaches.
Key UNIX principles, formed during its evolution, have significantly influenced the future of development. Principles like "Do one thing and do it well" have become fundamental for many modern software design methodologies.
The UNIX philosophy evolved alongside the UNIX system itself, gradually crystallizing into a set of clear principles. Over the years, many formulations of these ideas emerged, but their essence remained unchanged. Today, we’ll look at these key principles in their modern understanding:
Write programs that do one thing and do it well.
Write programs to work together.
Write programs that handle text streams, because that is a universal interface.
Although we're discussing the principles and philosophy of UNIX, for practical examples, we’ll use Linux, specifically Debian. This choice is due to Debian's free availability, ease of access (including on the Hostman platform), and its status as a classic example of a Linux system.
While Linux is not a direct descendant of UNIX, it inherits all its principles. Most of the commands and concepts discussed are applicable to both UNIX and Linux. It’s also worth noting that the popular Ubuntu distribution is derived from Debian, highlighting the latter's importance in the Linux world.
If you've worked with Linux systems, you're probably familiar with the cat
program (short for concatenate). Although it appears as a command in the bash (command line), it is actually a standalone program written in C, compiled, and usually located at /usr/bin/cat
. Its source code is publicly available online as part of the GNU coreutils
project.
Example of using cat
:
$ cat /etc/passwd
$
is the command line prompt displayed by the terminal and isn't typed by the user.
cat
is the program itself. We don't specify the full path because the $PATH
variable stores directories where BASH searches for commands by default (in this case, /usr/bin/
).
/etc/passwd
is a text file in Linux systems that contains user information.
The result of this command will be a list of system users, similar to:
root:x:0:0:root:/root:/bin/bash
alice:x:1000:1000:Alice Smith,,,:/home/alice:/bin/bash
bob:x:1001:1001:Bob Johnson,,,:/home/bob:/bin/zsh
mysql:x:112:120:MySQL Server,,,:/nonexistent:/bin/false
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Each line contains the username, UID, GID, full name, home directory, and other parameters, separated by colons.
You can experiment with cat and other commands by:
cat
has many additional options, for example:
-n
or --number
.-A
or --show-all
.-s
or --squeeze-blank
.You can explore other options by running cat --help
or reading the full documentation with man cat
.
One important feature of cat
is its ability to concatenate the contents of multiple files. This is a result of its main function: cat
reads the specified files sequentially and sends their contents to standard output (stdout
). This is where its name comes from — concatenate means "to link" or "to chain together." In essence, displaying content on the screen is a combination of the file's contents and the stdout
stream in Linux.
Example of using cat
to concatenate files:
cat /etc/hostname /etc/hosts
The output might look something like this:
myserver
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
In this example, cat
first displays the contents of /etc/hostname
, followed immediately by the contents of /etc/hosts
.
In this way, cat adheres to the first principle of UNIX: "Do one thing and do it well." It specializes in reading files and sending their contents to stdout without unnecessary complexity, maintaining the modular and efficient approach that defines the UNIX philosophy.
This principle showcases one of UNIX's key features — the ability to combine simple tools to solve complex tasks. Thanks to Doug McIlroy, who introduced the concept of pipes in UNIX, commands can be chained together. The output of one command can be the input for another. Let's look at an example:
cat /etc/passwd | grep user
grep
is another program (command) that filters a text stream and returns only the lines containing the specified text — in this case, user
. Here, cat
outputs all the lines of the /etc/passwd
file. grep
then checks each line; if the text user
is present, the program outputs that line.
The output would be the line corresponding to the user
account:
user:x:1000:1000:,,,:/home/user:/bin/bash
We can extend this process by adding more commands:
cat /etc/passwd | grep user | awk '{print $6}'
This command outputs the home directory of the user.
awk
is a special programming language for processing text data. In this case, the awk
program acts as an interpreter for this language. In this example, awk
splits the lines into columns using the default delimiter — a colon (:
) — and prints only the sixth column, which is the home directory:
/home/user
In UNIX, commands can be chained into long pipelines to perform complex operations. This modular approach allows each tool to focus on a specific task, working seamlessly together to achieve sophisticated results.
This principle emphasizes the importance of using text format for data exchange between programs. Text data is easily readable by both humans and machines, making it a universal interface.
In UNIX, most configuration files, logs, and command outputs are in text format. This allows users to easily view, edit, and process data using standard text tools.
Example of text stream processing:
du -h /var/log | sort -rh | head -n 5 | awk '{print $2 " - " $1}'
This complex command uses several programs that exchange data through text streams:
du -h /var/log
— Displays the sizes of files and directories in /var/log
in a human-readable format.sort -rh
— Sorts the output by size in reverse order.head -n 5
— Selects only the top five lines.awk '{print $2 " - " $1}'
— Reformats the output, displaying only the first two columns in reverse order and adding a dash between them.Example output:
/var/log/syslog - 200M
/var/log/auth.log - 150M
/var/log/kern.log - 100M
/var/log/dpkg.log - 50M
/var/log/faillog - 10M
Using text streams provides flexibility and universality when working with data. It allows users to easily combine different tools and build complex information processing systems while maintaining simplicity and efficiency in interactions between components.
This approach to text-based communication and modular design is at the heart of the UNIX philosophy, influencing many modern development practices, including the design of microservices.
The term "microservices" was first mentioned at a software architecture conference in Venice in 2011. Since then, microservices have become an integral part of modern software architecture. By 2015, industry giants like Netflix and Amazon had successfully implemented this approach. Since then, the popularity of microservices in IT has been steadily growing.
A monolith is short for "monolithic architecture." In this type of architecture, all the project’s code is typically stored in a single Git repository, worked on by all developers. This means that even a small change in one function requires redeploying the entire application.
Monolithic architecture is characterized by tight coupling between components, which makes independent scaling and updating challenging.
As the project grows, maintenance becomes more difficult, and build and test times increase.
Introducing new technologies is also harder because changing one part might affect the whole system.
Despite these drawbacks, monolithic architecture can be effective for small projects or in the early stages of development due to its simplicity and cohesiveness. Notably, some exceptions like Stack Overflow and Etsy successfully use monolithic architectures even at large scales.
Microservices replace monolithic architecture when a project grows so large that it becomes difficult to manage. In a monolith, every system deployment (uploading the project to the production server) requires coordination among all developers, and testing and building take a lot of time.
Microservices break the project down into modules, each performing a specific task. The principles of microservice architecture include:
For example, a user service is connected only to the user database and handles functions related solely to it, such as adding or updating users. Payment or analytics functions are managed by other microservices, which may have their own separate databases.
Over time, services become more complex as checks, validations, and new features are added. Each module can be assigned to a separate team, which resembles encapsulation in OOP (Object-Oriented Programming).
The independence of microservices allows teams to work autonomously, speeding up the development and deployment of new features. External developers only need to understand the interfaces without delving into internal details. This also accelerates testing and building.
Microservices allow the use of different programming languages, such as:
As mentioned earlier, each microservice can have its own database, for example:
Essentially, a microservice can function solely as an abstraction layer over a database.
An important advantage of microservices is the ease of horizontal scaling. This allows the system's capacity to be increased by adding new servers, which is typically cheaper and more efficient than vertical scaling (enhancing the power of individual servers). This approach provides flexibility and cost-efficiency as the load grows. We'll discuss this in more detail in one of the following sections.
Despite its advantages, the microservice architecture makes projects more complex. New challenges arise, such as ensuring reliable communication between components, securing data during transmission, and complicating the deployment process.
These challenges fall on the shoulders of DevOps specialists. They develop and implement strategies for effectively managing a distributed system, including monitoring, logging, and deployment automation.
They also implement CI (Continuous Integration) and CD (Continuous Delivery) practices. Although solving these problems requires additional resources, it provides the flexibility and scalability needed for the system's long-term growth.
The evolution of microservice architecture is closely linked to the development of tools for creating, deploying, and managing distributed systems. Containerization and container orchestration have become key technologies in this field.
Containerization is an OS-level virtualization method that allows isolated processes to run in a shared environment. Launched in 2013, Docker became synonymous with containerization, revolutionizing the way applications are developed and deployed.
Docker allows you to package an application with all its dependencies into a standardized unit of software — a container. Containers typically host individual microservices, making them ideal for microservice architecture. They are lightweight, start quickly, and ensure consistent runtime environments from development to production.
The standardization of containers led to the creation of the Open Container Initiative (OCI) in 2015, ensuring compatibility between different containerization tools.
With the growing popularity of containers, there arose a need for tools to manage large numbers of containers in distributed environments. This led to the concept of container orchestration.
Initially developed by Google and released in 2014, Kubernetes has become the de facto standard for container orchestration. It is a platform for automating the deployment, scaling, and management of containerized applications.
Key features of Kubernetes:
Kubernetes allows the creation of clusters — groups of computers working as a unified system. This makes it ideal for microservice architecture, enabling efficient management of the lifecycle of numerous, distributed microservices.
Modern microservice development relies on a variety of tools and services that simplify the creation, deployment, and management of distributed systems. Russian cloud providers like Hostman offer comprehensive solutions for working with microservices:
Cloud Servers and VDS/VPS: Hostman provides virtual machines with pay-as-you-go billing, making them perfect for flexible microservices scaling.
Kubernetes: Managed Kubernetes clusters in Hostman enable efficient container orchestration, automating deployment, scaling, and management of microservices.
Cloud Databases: The Database-as-a-Service solution simplifies data management in microservice architectures, ensuring high availability and scalability.
S3 Object Storage: This service provides reliable storage for large volumes of data, which is often required in microservice applications.
Load Balancers: Load distribution between servers is critical for maintaining microservices' performance and fault tolerance.
App Platform: This service simplifies the deployment of applications from repositories, streamlining the deployment process for microservices.
These tools and services allow developers to create reliable, scalable, and secure microservice applications. They provide the necessary infrastructure and management tools, enabling teams to focus on developing business logic rather than solving infrastructure challenges.
Monolithic architecture has several advantages, especially in the early stages of development:
Simplicity in Development: All the code is located in a single repository, simplifying the development and debugging processes.
Unified Code Base: All developers work on the same code base, which fosters a better understanding of the project as a whole.
Simplified Deployment: A monolith is deployed as a single application, streamlining the deployment process.
Ease of Testing: Integration testing is easier since all components are within one application.
Performance: In some cases, a monolith can be more performant due to the lack of network overhead between components.
However, as a project grows, microservice architecture begins to showcase its advantages:
Scalability: Each microservice can be scaled independently, optimizing resource usage.
Flexibility in Technology Choice: Different microservices can use the most suitable technology stack for their needs.
Independent Deployment: Services can be updated and deployed independently of each other, speeding up the development and release of new features.
Fault Isolation: Issues in one microservice do not affect the entire system.
Ease of Understanding and Maintenance: Each microservice is smaller and simpler than a monolith, making it easier to understand and maintain.
Aspect |
Monolith |
Microservices |
Development |
Easier in early stages |
More complex but more flexible as the project grows |
Deployment |
Simple but requires full updates |
More complex but allows for partial updates |
Scalability |
Vertical, entire application |
Horizontal, individual services |
Reliability |
One failure can affect the whole system |
Failures are isolated within individual services |
Tech Stack |
Unified for the whole application |
Can vary across different services |
Performance |
Potentially higher for small applications |
Can be optimized for large systems |
Team Collaboration |
Entire team works on one codebase |
Teams can work on separate services |
Choosing between monolithic and microservice architecture depends on the project's size, requirements for flexibility, and scalability.
Monolithic Architecture is often preferable for smaller projects or MVPs (Minimal Viable Products) due to its simplicity and ease of deployment.
Microservices are better suited for large, complex systems with high demands for scalability and flexibility.
The decision should be based on the specific needs and long-term goals of the project.
Let's look at an example of creating a system for autonomous vehicle management. In this system, a cluster of interconnected servers automatically distributes containers across servers, optimizing resource usage and ensuring fault tolerance.
For instance:
Computer Vision Containers will run on a computer with a powerful GPU, which is necessary for fast visual data processing.
Vehicle Monitoring Services require a reliable CPU and can, therefore, be placed on less powerful but stable hardware.
Other Microservices will be evenly distributed across the cluster.
This architecture creates a fault-tolerant system. If one node (a separate unit in the distributed network) fails, the microservices can automatically move to another computer within the vehicle. Essentially, this replicates cloud architecture on local devices (on-premise), ensuring system continuity even with partial hardware failure.
On each node of this cluster, containers are launched—isolated microservices performing their specific tasks. This ensures flexibility in resource distribution and system management, optimizing the operation of each component of the autonomous vehicle.
Each microservice operates autonomously and interacts with others through well-defined interfaces, providing several benefits:
For example, the Computer Vision Microservice is a critical module responsible for recognizing road signs, lane markings, other road users, and obstacles. Its accuracy directly impacts the safety of the driver, passengers, and other road users.
With a microservices architecture, development and improvement can focus solely on this module without affecting other system components. A specialized team of computer vision and machine learning experts can work exclusively on enhancing this module.
Imagine the team has developed a new machine learning model that significantly improves sign recognition under poor visibility conditions, such as fog or heavy rain. After thorough testing, only this specific module needs to be updated.
Moreover, the update can be deployed "Over the Air" (OTA) since only one microservice is updated, and the amount of data transferred is relatively small.
If the entire system had to be rebooted just to update the computer vision module, the vehicle would require a high-speed connection and a long time to download and install updates for the entire system.
Microservices architecture also enables easy scalability of individual system components. For example, if the new recognition model demands more computing power, the GPU capacity for the computer vision module can be increased without affecting other modules.
Additionally, this architecture enhances the system's fault tolerance. If the computer vision module fails during an update or operation, it won't lead to a total system crash. Other microservices can continue functioning, possibly using a previous version of the computer vision module or with limited functionality.
Each microservice can be implemented using the most suitable technologies for its tasks. For the computer vision module, specialized machine learning libraries such as TensorFlow or PyTorch, optimized for GPU operations, can be used.
Meanwhile, other modules—like the communication module—can be built using different programming languages and technologies better suited for their tasks.
The microservices architecture provides the flexibility, scalability, and efficiency needed to develop and maintain complex systems like autonomous vehicle management. It allows continuous improvement of individual components without risking the integrity of the entire system.
Despite the decades that separate the concepts of UNIX and microservices, parallels can be drawn between them. Comparing microservices with UNIX reveals both common principles and unique features of each approach. Both strive for modularity and specialization of components. Microservices, like UNIX utilities, often perform a single specific task, whether it's managing users or access or serving as an abstraction for a database. However, microservices are typically more complex and can grow with additional features.
The interaction of components is implemented differently: UNIX uses native stdin and stdout redirection through pipes, while microservices require specific protocols (REST, RPC) with clearly documented interfaces. This complicates communication between services compared to the simplicity of the UNIX approach.
However, both approaches often rely on a text-based format for data exchange. In microservices, this is typically JSON or YAML, aligning with the principle of text streams in UNIX.
These similarities and differences demonstrate the evolution of modularity and component interaction ideas in software development. Despite the time gap between their emergence, UNIX and microservices share many key concepts, highlighting the universality of certain development principles.
Modularity and single responsibility:
UNIX: Utilities perform one task and do it well.
Microservices: Each service is responsible for a specific function (user management, access, caching).
Component Interaction:
UNIX: Utilities work together through pipelines.
Microservices: Services interact via APIs.
Text-based data format:
UNIX: Uses text streams for data exchange.
Microservices: Often use text formats (JSON, YAML) for data exchange.
Component Complexity:
UNIX: Utilities are usually simple and perform a minimal set of functions.
Microservices: Can be more complex and accumulate additional features.
Interaction Mechanism:
UNIX: Native stdin and stdout redirection through pipes.
Microservices: Require data transfer protocols (REST, RPC) with clearly defined interfaces.
Execution Context:
UNIX: Typically runs on a single computer with minimal delays.
Microservices: Can be distributed across different servers and data centers.
Goals and Application:
UNIX: Focused on the stability and reliability of the operating system.
Microservices: Focus on business logic and application flexibility.
Development and Deployment Complexity:
UNIX: Relatively simple development and installation of utilities.
Microservices: Require complex infrastructure for development, testing, and deployment.
We have analyzed the UNIX philosophy and microservices architecture, identifying both similarities and differences between these approaches to software development. Despite being separated by decades, both approaches demonstrate remarkable unity in key principles.
The main similarities we found include:
Modularity: Both UNIX and microservices aim to divide functionality into small, manageable components.
Specialization: Both approaches support the idea that each component should perform one task and do it well.
Interaction: Both UNIX and microservices emphasize effective communication between components.
However, we also identified significant differences:
Context of application: UNIX is designed to work on a single computer, while microservices are intended for distributed systems.
Complexity: Microservices are generally more complex to develop and deploy compared to UNIX utilities.
Flexibility: Microservices provide greater flexibility in choosing technologies and scaling individual components.
These similarities and differences are not coincidental. They reflect the evolution of software development principles in response to changing needs and technological advancements. The UNIX philosophy, created in the era of mainframes, laid the groundwork for the modular approach that today finds new expression in microservices, meeting the demands of the cloud computing and distributed systems era.
The principles underlying the UNIX philosophy and microservices architecture are also reflected in other software development methodologies. Object-Oriented Programming (OOP), with its concept of encapsulation and the SOLID principles, emphasizes the importance of modularity and specialization. The Single Responsibility Principle (SRP) from SOLID resonates with UNIX’s idea of “doing one thing well.” Design patterns such as facade, adapter, and singleton promote the creation of modular and efficiently interacting components. Functional programming, with its focus on pure functions and immutability, also shares the idea of creating small, well-defined components, which aligns with the principles of both UNIX and microservices architecture.
For modern developers and architects, understanding these principles is critically important. It allows us to learn from the time-tested ideas of UNIX, adapting them to the contemporary demands of scalability and flexibility that microservices provide.
Looking to the future, we can expect further development of both approaches. We will likely see new tools and practices that will simplify the development and deployment of microservices, making them accessible to a wider range of projects. At the same time, the principles of UNIX are likely to remain relevant, continuing to influence the design of operating systems and development tools.