00:00:09.320
It's pretty common during job interviews to be asked to program FizzBuzz. Now that microservices have become such a popular movement, I think it's time we equip ourselves for a distributed FizzBuzz in the microservices interview context.
00:00:14.219
Before we begin, I want to review the rules of FizzBuzz. In a normal interview where you're asked to write a FizzBuzz program, you count from 1 to 100. Each time a number is divisible by 3, you output 'Fizz'; each time it's divisible by 5, you output 'Buzz'. If a number is divisible by both, like 15, you output 'FizzBuzz'. For all other numbers, you output the number itself.
00:00:18.269
Now, one thing that often happens when you need to extend a presentation duration and need some filler is to talk about the origins and history of the topic at hand. So, I'm going to discuss the origin of FizzBuzz. About 20 years ago, it was popularized as a children's game, but before that, it was actually a drinking game.
00:00:27.840
You and your friends would gather in a tavern, taking turns to say the next number or 'FizzBuzz' or whatever, and of course, you would lose if you got it wrong. The lesson I derive from this is that any game suitable for adults while drinking is also suitable for children when sober.
00:00:39.870
With that in mind, let me describe a scenario where two people are playing FizzBuzz together: Player 1 and Player 2 are at the tavern, having a round of beers. Player 1 decides to throw down the gauntlet and challenges Player 2 to a duel of FizzBuzz. He starts with '1'.
00:00:52.079
Player 2 responds with '2' because she's always willing to take up a challenge, so the game is afoot. Player 1 continues with '3', and then they both realize how boring this game is. They decide to make some unfortunate decisions.
00:01:01.300
Player 2 resumes the challenge and musters her best effort. However, four rounds in, Player 1 fails miserably. This serves as a model for a FizzBuzz microservice experience. Instead of having one program count from 1 to 100, we simulate having different players or clients making submissions.
00:01:07.859
The clients of the microservice—whether that's another microservice or a web frontend—will submit their answers. The service keeps track of the game state, and whenever a client submits a correct answer, the service advances its counter and publishes an event message. Conversely, if a client submits an incorrect answer, the service rejects that turn.
00:01:24.889
The command and event messages we send constitute the API or interface of this service. We interact with it through these messages. However, while sending messages, we must acknowledge the reality of distributed systems, particularly that messages are sent over potentially unreliable networks.
00:01:41.610
We can count on messages failing to arrive at times, arriving multiple times, or arriving in a different order than they were sent. This is an undeniable reality due to the laws of physics. Because latency is not zero, services can only really operate with their own local data. They can't rely on querying other services as the information may become outdated by the time a response is received.
00:02:00.380
This informs a significant design principle in distributed systems: we need our microservices to handle failures gracefully and account for failure modes that don't exist in fully synchronous systems. This underlying reality is what complicates the FizzBuzz implementation. In traditional programming, FizzBuzz is straightforward—typically about 17 lines of code—however, with networking and messaging elements, we have to consider failures or face consequences.
00:02:15.960
Moreover, synchronous request/response patterns, such as HTTP GET calls, are no longer suitable due to the risks of out-of-date data. Setting this up leads to a structure where clients send command messages over a command queue to the FizzBuzz service, which will then publish events.
00:02:31.200
Let's delve into some code. We begin with our message objects: command messages and event messages. They function as schemas and contracts that define how services exchange information.
00:02:38.420
These command and event messages often share common attributes. For instance, the game ID, answer, and time are frequently copied from the command message into the event message. Additionally, our event messages contain two timestamps: one for when the client requested the turn and another for when the service processed that command.
00:02:56.559
It's essential to maintain awareness of these two distinct timestamps. So let’s examine the schemas for our command and event messages. The 'Take Turn' command allows you to start a new FizzBuzz game by taking the first turn. If the turn is accepted, the service publishes 'Turn Taken' for all to see. However, if an answer is wrong, the service will record and publish 'Turn Rejected'.
00:03:13.160
There are exit criteria for the game: either the counter reaches 100 successfully, or someone makes an incorrect move, resulting in the game ending abruptly. The next element to discuss is the entity, which I will demonstrate with some real code.
00:03:42.520
Here we have our entity—a simple data structure akin to a model in Rails, but without any persistence layer attached. To excel in an interview, separating your persistence layer from your entity layer can leave a positive impression on your interviewer. This entity features a command method, 'Take Turn,' and a predicate method, 'Correct,' which determines if a proposed answer is correct.
00:03:56.239
The core logic of FizzBuzz resides in the 'Correct' predicate method. This entity keeps track of the game state and can confirm whether someone provided a correct answer. That's sufficient for the entity. Next, I’ll show you the command handler, keeping in mind that clients are sending command messages over the command queue to the FizzBuzz service.
00:04:08.740
The command handler class is responsible for processing that message type. The first thing we notice is a lot of boilerplate code—demonstrating capability in writing comprehensive classes can impress interviewers as well.
00:04:23.780
Beneath the boilerplate, we have our business logic. Here, when we take the turn, we retrieve the game entity from a store or repository. Based on whether the submitted answer is correct, we publish 'Turn Taken' or 'Turn Rejected' events and write the respective event to the event stream. Don’t worry if it seems overly complex; greater complexity often correlates with impressing your audience.
00:04:39.139
Moreover, there’s a semblance of optimistic locking at play. When we fetch the entity from the store, we obtain a version and must account for it when putting it back in. In interviews, highlighting your understanding of mechanical details such as optimistic concurrency can be advantageous.
00:04:55.380
Having demonstrated an entity without a persistence layer and how it fetches from the store, we also need to address what happens with the events we publish. This is often achieved through event sourcing, where the state of our entities is tracked. We're working with something called a projection—let me explain how this works.
00:05:12.740
Imagine a projection being actuated by a newly formed game entity. It reads all published events pertaining to that game in the past and applies each event to the entity to maintain accurate state. For example, we set the game ID and retrieve timestamps, which are packaged as ISO 8601 strings. Knowledge of these standards can really impress during an interview.
00:05:23.700
As we take the turn—an event advancing our game state—we’ll set the timestamp when the game ends. Now let's touch on event handlers, which will closely resemble command handlers since both essentially manage messages.
00:05:37.420
In our event handler, we handle the 'Turn Taken' messages by querying the game entity for victory conditions. After verifying if a game has met its exit criteria—like reaching a hundred turns or the game already ending—we will write a 'Game Ended' event.
00:05:52.059
To summarize, we handle every 'Turn Taken' event, but we only publish the 'Game Ended' event when the criteria are met. On the other hand, if a 'Turn Rejected' event occurs, it will end the game abruptly per the established rules.
00:06:02.380
Now let’s proceed to a live coding demonstration, a potentially embarrassing experience, which I’m more than willing to embrace.
00:06:11.410
On the top pane, I have the FizzBuzz service running, while the bottom pane shows an interactive IRB console where I can submit commands using helper methods that will issue command messages for me. The game hasn’t started yet, so I’m going to provide the first value.
00:06:27.430
Does anyone want to wager what the next correct answer is? Is it 'Buzz'? No, it’s not; it’s 'Fizz'. There we go—that’s our demo.
00:06:33.310
Thank you, but wait—there's more! I've discussed the fallacies in distributed systems, emphasizing that messages may arrive out of order or multiple times. None of the code demonstrated is inherently equipped to handle these scenarios.
00:06:47.260
What this suggests is that I haven’t built idempotence into the turn-taking system. Thus, if we issue a 'Take Turn' command, it could be processed multiple times, presenting a significant issue.
00:06:59.310
We're now at a crucial point—having aced the interview, the discussion shifts to negotiating your terms where you can introduce important design patterns for system architecture. The first is the Idempotence Key pattern, which I'm very passionate about.
00:07:15.330
Next, we'll discuss the Reservation pattern, followed by the Sequence Number pattern. We'll seamlessly fold these into the previously established code, and it’s going to be simple and effective.
00:07:30.550
At this point, Nathan becomes involved in what could be an amusing and somewhat contentious exchange regarding the practicality and realities of microservices.
00:07:41.040
Nathan questions Scott, expressing skepticism about the validity of his statements regarding microservices architecture and language flexibility, noting how teams often fall into routines tied to their programming languages.
00:08:01.210
Their conversation continues, touching on why microservices shouldn’t be restricted to particular languages and the nuances of APIs versus messages in effective service autonomy.
00:08:15.850
Nathan raises a point about the common association of microservices with HTTP APIs, which Scott addresses by emphasizing autonomy and the importance of understanding the underlying mechanisms of distributed systems.
00:08:29.280
Scott explains that while microservices can have web APIs, a focus on understanding phenomena like idempotence is crucial for avoiding pitfalls leading to distributed monoliths—situations where the systems become overly intertwined, creating additional challenges.
00:08:59.180
The dialogue between Nathan and Scott outlines the complexity of distributed systems and their architecture across various programming environments, ultimately leading into a mention of an upcoming workshop on distributed systems theory and practice.
00:09:13.600
Scott shares details of the workshop, providing a discount code for attendees, emphasizing the extensive knowledge they'll gain regarding building scalable and autonomous systems with Ruby.
00:09:27.540
The session concludes with thanks to the audience and applause, laboring the importance of continuous learning and collaborative development practices in understanding microservices and distributed systems in depth.