Should Startups Be Using Microservices? A Presentation From Zuka Kakabadze
Scalac invited Zuka Kakabadze, Founder and CTO of digital signage provider Fugo.ai, to speak at its June 2021 microservices event “Between Business & Tech”. The digital conference featured six microservice experts who shared their perspectives on the implementation of a microservices architecture. Follow this link to watch Zuka, and the other speakers’ presentations.
Zuka is a software engineer and computer scientist who worked at several different startups before launching Fugo. Since its founding in 2017, Fugo has built a presence in over sixty countries and operates in a wide variety of sectors.
In his presentation, Zuka shared his journey of launching a startup and talked about key software decisions that helped the business grow. In addition, he covered common problems faced by startups and greenfield software projects.
The initial architectural decisions that startups make have long-lasting consequences. Much like laying the foundations of a house, every decision has the potential to either hurt or harm a business.
Zuka discussed the ways in which he leveraged Scala, Zio, and Scalac to achieve his vision of functional architecture in his codebase.
What problem is Fugo solving?
Digital signage is everywhere – advertising screens at malls, fast food displays at takeaway outlets, airport departure monitors, and so on. In 2019 alone, two hundred and forty million screens were sold.
Because of its extensive reach, digital signage is critical for advertising and the dissemination of information. Digital signs attract new customers, engage existing ones, and drive sales. In many cases, signs are especially crucial because they act as the primary medium between customers and companies.
Half of all screens sold are used primarily in public or business settings. However, these screens aren’t connected to software and require manual management using USB sticks or other DIY solutions. This can lead to problems, such as outdated content remaining visible or screens being left empty. What’s more, unintelligent signage can disengage an advertiser’s audience and reduce the return on investment.
Fugo wanted to transform these unconnected, unintelligent machines into smart devices.
How does Fugo solve this problem?
Fugo solves the problems that arise when screens must be managed manually in the following ways:
- Fugo streamlines the management of streams and content. Instead of using manually controlled signs, Fugo’s content management system (CMS) allows businesses to manage intelligent signs remotely, irrespective of distance.
- Fugo utilizes business-endorsed tools and low-cost, non-specialized hardware.
- Fugo’s AI can measure audience impact and display targeted content.
- Fugo provides access to a range of features: media with touch screen content, responsive displays, business application integration, and rich content templates. What’s more, businesses can create, publish, monitor, and measure their audience’s engagement with signs.
- Fugo provides screens built on robust and stable technology that is largely fault-proof, preventing failures such as the Gatwick airport 2018 screen outage that delayed flights for days.
What technology does Fugo leverage?
Fugo defined its customers’ needs by going through a rigorous build-measure-learn cycle. By creating a minimum viable product (MVP), taking it to market, and testing the results, the Fugo team quickly identified viable niches.
Fugo recognized the need for a repeatable model that was quick to market, extensible, and scalable. As a result, it utilized the latest tools available, along with Scalac, to build a hybrid microservice architecture. The dev team applied high-level functional design patterns to create a modular system that could be mapped onto physical architecture.
Fugo’s functional architecture
In the early stages of development, Fugo’s engineers didn’t devote large amounts of time to deciding whether or not they should build their core software architecture on monolith or microservice structures.
Instead, they prioritized aligning their software design with Fugo’s business requirements by creating an abstract, logical architecture that was indifferent to the physical architecture.
Fugo’s engineers believed that good architecture should clearly define the barriers between containers, modules, and components to minimize the cost of new additions.
With all that in mind, Fugo based its software design on functional architecture for the following reasons:
- Functional architecture reduces the complexity of performance while improving modularity and testability.
- It simplifies maintenance.
- Functional architecture automatically ensures high levels of cohesion and low coupling without having to enforce team discipline.
- Functional programming (FP) organizes code at the low level but is also useful for more high-level projects.
- FP breaks problems down into manageable chunks that are easy to solve and build into a larger solution. This composition framework means that code can be reused in other areas.
Now let’s take a closer look at the main components of the kind of functional architecture that Fugo uses.
Workflows that can be deployed into stand-alone containers comprise one of the basic features of functional architecture. In contrast to the request-response model found in an object-oriented method, workflows are one-directional pipelines with inputs and outputs. Workflows are made up of pure functions that act as independent units and only contain necessary functionality. Like functions, workflows compose at a low level.
Fugo utilizes workflows that follow a specific process: the workflow receives a command as an input, makes a business decision, and then transforms the data before outputting a new event.
User Experience (UX)
In terms of its broader user experience (UX) strategy, Fugo streamlined the design process by dividing ideas into “must-haves”, “good-to-haves”, and “future features”. Fugo did this on a granular level to reduce complexity so that developers could change individual sections without affecting the whole system. For example, UX specialists were able to modify the image upload feature without affecting screen pairing.
Fugo combined every proposed core feature of the UX design into a unified workflow. Its software engineers then grouped the workflows into logical units and moved ahead with a domain-driven design (DDD) model.
Domain-Driven Design and Bounded Contexts
DDD creates a mental model with cohesive terminology that is shared across business and tech domains. The shared model is clearly evident in the structure of Fugo’s code.
Fugo utilized bounded contexts to group their workflows. The word “context” refers to a contained and clarified design, while “bounding” refers to the reduced coupling between subsystems.
Each bounded context is autonomous and capable of making decisions in isolation. Therefore, if one bounded context fails, others can continue functioning independently, thereby removing any single point of failure. A single team or developer is designated to each bounded context to maintain simplicity.
There are several different ways that bounded contexts can be implemented:
- In monolithic systems, bounded contexts can be as simple as separate modules in a single container.
- Developers can deploy bounded contexts in separate containers, as seen in server-oriented architecture.
- Each workflow is treated as a standalone deployable container, like in microservice architecture.
The diagram above shows a simplified version of how Fugo chose to use DDD to divide their bounded contexts. As the contexts are separate, some Fugo features require multiple workflows from different bounded contexts.
For Fugo, workflows within bounded contexts are triggered by events in the logical view, rather than the physical view. Events are implemented using either a request-driven command and control protocol that increases coupling or through event-driven queues and message buses that reduce coupling. Alternatively, engineers may use a hybrid of both approaches.
How is code structured in a bounded context?
Many engineers try to use a traditional, layered approach to architecture design. In layered architecture, the infrastructure, service, domain, and database components each have workflows that move through every layer.
However, this approach means that any change to an individual workflow affects other layers and workflows. This significantly reduces developers’ ability to extend and iterate through workflows.
Instead of using layered architecture, Fugo leveraged an “onion” structure with a functional flow. The infrastructure comprises the outer layer of the onion and the domain makes up the inner layer, as seen below.
In the example of onion architecture shown above, the impure infrastructure code can access the core domain code. However, the domain code cannot access the infrastructure code. In addition, the I/Os are at the edges of the onion.
The domain code consists of pure functions that operate using abstract data types (ADTs). Developers can use ADTs to represent the domain in a fine-grain and self-documenting way.
Developers also use types to encode business rules to prevent the creation of incorrect code. It’s also possible to leverage static type checking in immediate unit tests to make sure the code is correct at all times.
This onion approach increases time-to-market speed and code reliability. What’s more, developers can roll out any future changes with greater confidence and efficiency.
The Fugo workflow pipeline
Let’s take a look at a specific example of a Fugo workflow. Here’s how Fugo publishes a media playlist for display on digital signs:
- Get a playlist from a repository.
- Send an event that notifies the system that the playlist is published.
- Log that the playlist is published.
- Return the playlist.
Fugo code has a function for each step. What’s more, each step must be stateless and isolated so that it can be tested and reasoned independently.
However, chaining functions into workflows is challenging because the outputs from one function won’t match the inputs of the next function in the pipeline. This happens for two reasons. Firstly, functions have dependencies and additional parameters that are not part of the workflow timeline. Secondly, functions have several outcomes (such as errors).
Until recently, building this type of structure was unimaginable in a startup due to its complexity. In order to develop a functional architecture, a functional programming language is needed. Because of this, Fugo decided to utilize Scala and Zio to build its system.
Fugo utilized Scala for the following reasons:
- Scala is a powerful and expressive language with a strong type system.
- Scala allows Fugo to use existing Java Virtual Machine infrastructure, which is vital for a startup because time-to-market needs to be short.
- Scala’s concurrent and parallel programming tools allow for better utilization of service resources, reducing AWS fees.
Fugo chose to combine Scala with Zio for the following reasons:
- ZIO offers reasonable and composable abstractions.
- It provides Fugo with the ability to apply functional programming to its high-level design and architecture.
- ZIO works well with existing Scala and Java frameworks and libraries.
- It helps to maintain a consistent codebase and forces developers to stick to functional programming, while other methods require high levels of team discipline and coordination, which startups often lack.
Below is a “publish playlist” workflow written using Zio. It features a Zio library centered around Zio types. This particular Zio instance takes an input and outputs either an error or desired value.
Zio has three argument types:
- Input type “R” – an environment or dependency
- Output type “E” – an error
- Value type “A” – you successful value
The publish playlist function sequentially composes all the sub-functions, which are also Zio types.
If the publish playlist function is successful, it returns a playlist. However, if there is a failure at any stage in the pipeline, it produces an error type. Dependencies must be provided to run the publish playlist function.
One way to create dependencies in Zio is by utilizing the Zlayer feature. This feature enables developers to build a graph of dependencies using services.
The example above shows a playlist queue service API that exists in the domain layer and an Amazon Simple Queue Service (SQS) implementation of the playlist service that remains in the infrastructure layer.
Developers insert these graphs into modules to make the Zio workflow ready to run. For each bounded context, a dependency graph is constructed and inserted into a Scala module.
Each module is run either as a separate microservice or a modular monolith. For modular monoliths, ZLayers allow for the sharing of dependencies across modules to save resources. A layer graph must be provided to run a Fugo publish playlist workflow, as shown below.
Fugo’s physical architecture
Once any system’s logical architecture is completed, technical requirements demand consideration. Engineers must map the logical architecture to a physical architecture. Each Fugo bounded context or module has different requirements when it comes to scaling, availability, and behavior.
Currently, Fugo has eight microservices running on AWS infrastructure and one modular monolith called Fugo CMS. Fugo CMS was deployed as a modular monolith because it wasn’t initially clear which workflow would need scaling. Each module in the monolith has its own dynamo DB table and SQS queue that can scale with the module when needed.
Fugo used a monolith because microservices have operational overheads. However, engineers mitigate these overheads by automating parts of the infrastructure and using cloud services to their fullest extent (by using tools like Kubernetes and Fargate). Automation allows businesses to focus more on the specific business code and spend less time on the infrastructure code.
Fugo developers also recognized that the router module that coordinates screens needed to be a single item that could be deployed as a separate microservice. The separation between the Fugo CMS and the router was necessary because of their respective scaling requirements, which were vastly different. Fugo did not want the router’s performance to affect the UX or the functioning of the Fugo CMS to affect the screen functionality.
Fugo split its system into bounded contexts then deployed the modules in a single container. Developers do this using sbt, a Scala build tool. Furthermore, if a module requires scaling in the future, developers can change the code quickly and separate the monolith into different containers.
Monolith vs Microservice
Fugo had three main considerations when deciding between monolith and microservice architectures:
- Fugo doesn’t believe that the decision to choose a monolith or microservice is black and white. Rather, it depends on each individual use case.
- With the use of logical architecture, engineers can choose a mix of approaches that can be adapted according to the business’s needs.
- Using a functional workflow helps with physical infrastructure decisions and facilitates more effective organization.
Fugo doesn’t believe there are any easy answers when it comes to building software architecture. Factors such as the stage of the startup, quantity of resources, and budget all come into play when making critical choices.
Companies without software engineers at the helm can leverage tools such as Scalac to help them create high-quality systems that won’t need to be rewritten.
Furthermore, having a well-made product with reliable code provides startups with the confidence to approach clients of all sizes.
- [Video] Should startups be using microservices? A Presentation from Zuka Kakabadze
- [Slides] Should startups be using microservices? A Presentation from Zuka Kakabadze