CSCI 3325
Distributed Systems

Bowdoin College
Fall 2026
Instructor: Sean Barker

Project 2 - Online Bookstore

Release Date:Wednesday, October 14.
Due Date:Wednesday, October 28, 11:59 pm.
Collaboration Policy:Level 1
Group Policy:Groups of 2 or 3

In this project, you will design and implement a multi-tier distributed system using remote procedure calls (RPCs). You will then investigate how concurrent requests affect the performance of your system.

This project should be done in teams of two or three (unless otherwise cleared with me). As in Project 1, all team members are expected to work on all parts of the project.

System Specification

You have been tasked to design Nile.com: the world's smallest online bookstore. Since Nile.com aims to become a future tech giant, you will need to follow sound design principles in building the online store to allow for future growth.

The architecture of the system is illustrated in the figure below. The store will employ a two tier design consisting of a front end and a back end. The front end server will accept client requests and direct them to the appropriate back end service. The back end consists of two components: a catalog server and an order server. The catalog server maintains information on current inventory, while the order server is responsible for keeping the catalog appropriately updated.

architecture

Bookstore inventory is tracked within the system as a series of book entries, where each book entry consists of four attributes:

  1. an item number (e.g., 793)
  2. the book title (e.g., "Socket Programming for Dummies")
  3. the book topic/category (e.g., "self-help")
  4. the number of copies currently in stock (e.g., 25)

The various components of the system will interact with the inventory via the operations detailed below.

Server Operations

The front end server should offer three operations to clients:

Note that the front end server forwards searches and lookups to the catalog server and purchase requests to the order server; it should not maintain its own inventory data.

The catalog server should also support three operations:

The order server should support a single operation:

Operations must be correct even with respect to multiple concurrent clients. For example, if one copy of a book remains and two clients concurrently attempt to purchase it, exactly one of the attempts must succeed.

Other implementation details not specified above, such as the particular types involved and how errors are handled, are left for you to design.

Data Requirements

Choose your own initial catalog and stock quantities. A small catalog (for example, 10 books across 3 topics) is sufficient, although your data structures should not depend on those particular books.

New stock should arrive automatically at regular intervals. For example, you might add one copy of every book every 30 seconds. The precise policy is up to you; make sure periodic restocking does not corrupt inventory while client requests are running.

While a real service would probably store the catalog using a database service or other persistent storage, you do not need to implement that here. Instead, the catalog can reside entirely in memory in the catalog server.

Technical Requirements

You will implement all three servers in Java. You will also implement two different command-line clients (one in Java and one in Python), which will both communicate with the same front end server.

All communincation between distinct components will use gRPC, which is an open source, cross-platform RPC framework originally developed by Google. The gRPC framework runs on top of HTTP/2 (a modern successor to the standard HTTP protocol) and uses Protocol Buffers (a compact data format also developed by Google) as the data encoding and interface description language (IDL). Both clients (Java and Python) will use the same interface definitions.

To limit the complexity involved in using the gRPC framework, we will restrict ourselves to the following subset of gRPC functionality:

No graphical interfaces are required for any servers or clients. Each client should have a simple, usable command-line interface for searching, looking up books, and attempting purchases. Display query results legibly and make request successes or failures clear.

Your system must handle multiple client queries concurrently. One client's search or lookup should not automatically prevent another client's query from making progress. Purchases may be processed one at a time (i.e., the order server does not need to actually process purchases concurrently), but inventory must remain consistent despite simultaneous requests and periodic restocking.

Programs that contact other services must accept the relevant hostnames as command-line arguments:

You may use fixed port numbers, but these should be different for each server. Start the components in dependency order: catalog, order, front end, then clients. Your system should work both when all components run on one machine and when they run on separate machines.

Implementation Advice

gRPC and Protocol Buffers

With gRPC, you first describe a remote interface in a .proto file. The Protocol Buffers compiler and gRPC tools generate classes for the request/response messages, along with client stubs and server-side interfaces. Your application implements the server-side methods and calls the generated client stubs to make remote requests.

For example, a portion of a front end interface might look like this:

// a remote service specifying available RPC calls
service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply);
}

// format of a request message
message HelloRequest {
  string name = 1; // field 1: a string
}

// format of a reply message
message HelloReply {
  string message = 1; // field 1: a string
  int32 id = 2; // field 2: a 32-bit int
}

You are responsible for defining all the services and messages needed by the front end, catalog, and order servers. A single .proto file containing several services is fine, as are multiple smaller files. Choose message fields that make it possible to report successful results and expected failures unambiguously.

The gRPC tools will use a protobuf specification like the above to generate Java classes that you can invoke very much like a local method:

// connection to RPC server
ManagedChannel channel = ManagedChannelBuilder.forAddress(hostname, port).usePlaintext().build();

// get the stub object
GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);

// construct the request object
HelloRequest request = HelloRequest.newBuilder().setName(clientName).build();

// submit the RPC and get the reply object
HelloReply reply = stub.sayHello(request);

// print out the reply message and ID
System.out.println("Server said: " + reply.getMessage());
System.out.println("Server sent ID: " + reply.getId());

// close the connection
channel.shutdown();

In this example, GreeterGrpc, HelloRequest, and HelloReply are generated classes. Your Java server will implement the generated service base class and respond to requests using a StreamObserver. For example:

public class GreeterService extends GreeterGrpc.GreeterImplBase {
    @Override
    public void sayHello(HelloRequest request, StreamObserver response) {

      // get the submitted name from the request object
      String clientName = request.getName();
    
      // assign this message an ID
      int msgId = 3325;

      System.out.println("Responding to " + clientName + " with ID " + msgId);

      // construct the reply object
      HelloReply reply = HelloReply.newBuilder()
          .setMessage("Hello, " + clientName + "!").setId(msgId).build();

      // send the response; for a unary stream, call exactly once
      response.onNext(reply);

      // signal that the RPC is complete
      response.onCompleted();
    }   
}

Observe that the onNext() method is used to send replies; for unary (rather than streaming) RPCs, you will call this method exactly once, followed by onCompleted(), to return its single response.

The equivalent Python client uses the same .proto definition to generate compatible message classes and stubs:

with grpc.insecure_channel(f'{host}:{port}') as channel:
    stub = hello_pb2_grpc.GreeterStub(channel)
    reply = stub.SayHello(hello_pb2.HelloRequest(name=client_name))
    print(f'Server said: {reply.message}')
    print(f'Server sent ID: {reply.id}')

The code snippets above omit imports and various other boilerplate code; refer to the complete demo code discussed in class (and provided in your starter repository). Also refer to the official gRPC Java quick start, gRPC Java basics tutorial, gRPC Python quick start, and Protocol Buffers language guide.

Building and Running Your Code

Your system will use the Gradle build tool to download dependency files and generate Java classes from the .proto files. To make things easier for you, everything that you need is already installed on the class server and a few helper scripts are provided in the starter code to make it easier to build and run your code without getting too involved with Gradle itself.

Inside your repository, your code will all be located within src: Java code, Python code, and protobuf files inside java, python, and proto, respectively. To build the code, from the base directory, run make, which will invoke gradle to build the Java code and generate the protobuf classes as well as Python to generate the Python-specific protobuf files. Remember to re-run make to rebuild your code if you make any changes either to the Java files or to the protobuf files.

Once the code is built, you can execute a Java class easily using the provided runj script (which just invokes java using the proper classpath). For example:

./runj FrontEndServer arg1 arg2

You can similarly use the provided runp script to easily invoke a Python script, e.g.:

./runp client.py arg1 arg2

You should not need to change any existing files in the base directory (build.gradle, Makefile, runj, or runp), but feel free to look in these files to understand how things are working.

A small working gRPC client/server example with the same build setup is provided inside the demo directory. Start by building and running that example and make sure that you fully understand the code before beginning to implement your bookstore.

RPC Calls and Server State

Each unary RPC represents one logically independent request/response operation. This does not mean your server loses its application state after every request. A gRPC Java server typically saves a service implementation object that is used to handle many requests, potentially on different threads. For example, the catalog's inventory should persist in shared application state across calls.

Likewise, a client can reuse a gRPC channel for many requests rather than opening a new channel for every call. For your ordinary Java client and performance-test clients, keeping a channel and blocking stub for repeated calls is a good starting point.

If a backend server makes RPCs to another server, it is also a gRPC client from that server's perspective. For example, the order server implements its own buy RPC while using a catalog stub to make its downstream calls. In all of your servers, since the stub programs handle the networking details for you, you do not need any HTTP parsing or socket programming of your own.

Concurrency and Synchronization

Your servers will need to be careful about protecting data that may be accessed by multiple requests concurrenctly. In Java, a standard way to make code safe for concurrent access is via the synchronized keyword, which allows you to mark specific methods as only safe for execution by one thread at a time. Doing so ensures that the method will not be executed concurrently, which is good for safety but potentially bad for performance. Thus, the essential challenge of synchronization is to provide safety when necessary but still allow for enough parallelism to achieve good performance.

You will likely handle many gRPC calls using the same service implementation object, and gRPC can dispatch multiple calls to a remote service concurrently. This is useful for parallel queries, but it also means that multiple Java threads may access the same data structures at once. Protect shared state where necessary. For example, catalog updates should not proceed at the same time as reads, and two purchase requests must not both buy the final remaining copy.

You may find Oracle's Java synchronization tutorial useful if you have not done much concurrent programming previously.

Testing and Evaluation

For initial testing, you should run all components of the system on the same machine. For later testing, you will be provided with a set of four machines for running each component of the system separately - details on these machines will be provided later. You will need to run a set of performance experiments and provide results in your writeup (described below).

Project Writeup

Similar to Project 1, you will write a README describing important aspects of your project. You will also need to run some experiments and detail the results of those experiments in your writeup.

Your writeup should contain your names and the following sections:

  1. Design Decisions: Explain and justify any significant design decisions that you made. As before, you should not re-state small details of course; just stick to the bigger picture. For example, you should probably summarize your core data structure design maintaining the catalog, your protocol buffer interfaces, as well as your concurrency design for handling multiple requests. If you made other significant design decisions not mandated by the project specification, explain those too.
  2. Experimental Results: Run the following experiments on your system and document both your experimental design and the results you collected:

    1. Compute the average response time per client search request by measuring the response time seen by a client for many sequential requests.
    2. Now compute the average response time per buy request for many sequential requests.
    3. Rerun experiments 1) and 2) with multiple clients concurrently making requests to the system. Does your average response time change as the number of concurrent requests changes?
    The exact setup of how you run your experiments is up to you, but your experimental design should be sufficient for answering these questions, and your setup should be described in your writeup in enough detail that someone else would be able to replicate your experiments. As in the last project, generate graphs that clearly demonstrate your results.
  3. Known Bugs: List any bugs or outstanding issues that you encountered during testing.

A good rule of thumb for running real-world experiments is to be skeptical of the results; what might explain the results you see, even if they weren't necessarily what you expected? Note that this isn't to say that you should necessarily expect to be surprised by your results, but it is almost always the case that real-world experiments don't perfectly demonstrate what we expect (even if the underlying principle you're trying to demonstrate is sound). If you don't find that your results don't demonstrate what you expected, explain why you think that might be.

Experimentation Advice

For the purpose of testing your system, you will want to write one or more "test clients" that issue requests rapidly and automatically to the system. A purely interactive client like the "real" clients will not allow you to generate any meaningful load on the system (due to the time involved in manually specifying a query). However, you can very easily write a non-interactive client that issues many requests and collects timing statistics. You can use multiple threads or multiple processes to simulate concurrent client loads.

Even with a custom test client, you may have a difficult time establishing clear trend lines when comparing buys and searches due to the fact that both are cheap operations with a small database. In principle, to see clear trends as you increase the client load, you would need to massively increase the number of clients (real or simulated) to exert a real demand on the system. Unfortunately, given how cheap your operations likely are, you will probably be unable to generate a sufficient client load to get good results, even if you are using a powerful machine for client generation.

As such, you may wish to consider the following approach for experimental purposes: artificially make your catalog operations more expensive by adding small sleep delays in their respective catalog server methods. The simplest way to do this is to use Thread.sleep, e.g.:

Thread.sleep(100); // go to sleep for 100 ms
If you put this line inside your buy method and search method, it will have the effect of making both take longer, which will simulate more expensive catalog operations. Doing so will make it much easier to see the trend lines between buys and searches (without inherently advantaging one over the other, assuming you sleep for the same duration in each). 100 ms is probably a suitable sleep duration for this. For buys, make sure you sleep inside any synchronization block so that you can see the impact on concurrency. This technique this will make it easy to generate enough concurrency in your experiments to collect useful results.

Logistics and Evaluation

As usual, your starter repository will be initialized and shared once you confirm your group members with me. You should initially work and test on the class server, and then move over to the multiple other machines to be provided (particularly for the purpose of running experiments).

Also as in the first project, in addition to evaluating your submitted project, I will evaluate your project in an oral review session following the due date. The structure of this review will be similar to that of Project 1; please ask if you have any questions about what to expect. All the same guidelines as before apply (e.g., do not discuss your review session with any other groups until all reviews are complete.

Your project will be graded on (1) correctly implementing the server specification, (2) the design and style of your program, (3) your experimental design and results presentation, and (4) your oral review session. For guidance on what constitutes good coding design and style, see the Coding Design & Style Guide, which lists many common things to look for. Please ask if you have any other questions about design or style issues. Also don't forget to submit your individual group reports prior to the deadline.