Building a Beer Vending Machine Program with RavenDB Embedded Server
This guide will walk us through the steps to create a simple beer vending machine program using RavenDB’s embedded server.
RavenDB’s embedded server allows us to integrate a feature-rich, fully functional database directly within our application, eliminating the need for a separate server setup.
It can run on a wide array of devices, including Raspberry Pi, macOS, Windows, and Linux, making it an ideal solution for Point of Sale (POS) systems, Internet of Things (IoT) applications, and other embedded systems.
Thanks to its flexible ISV licensing model, RavenDB has become a popular choice across various industries, providing robust, scalable, and efficient data management solutions to meet diverse needs.
We’ll cover setting up RavenDB, defining our data model, loading, storing and patching our data.
Step 1: Setting Up RavenDB Embedded Server
Create a new .NET Console Application via Visual Studio or another IDE of your choice.
Install the RavenDB. Embedded NuGet package through the package manager console:
PM> Install-Package RavenDB.Embedded
Or through the Nuget Package Manager:
Add the following code in Program.cs to set up the embedded RavenDB server.
You can obtain a free license here (paste it into a license.json file).
// Start RavenDB Embedded Server
var serverOptions = new ServerOptions()
{
ServerUrl = "http://localhost:8080",
DataDirectory = @"Path\To\EmbeddedServer\Data",
Licensing = new ServerOptions.LicensingOptions()
{
LicensePath = @"Path\To\License\license.json"
}
};
EmbeddedServer.Instance.StartServer(serverOptions);
using var server = EmbeddedServer.Instance;
For more information about embedded server configuration options, check out our documentation here.
For additional options of registering a license visit this page.
Step 2: Defining the Data Model
Define the following models for our vending machine program:
- Machine: Will hold details about our machine and some settings and options.
- Beer: Represents a beer for sale in the machine. Including its number in the machine, its current price and how many units of this product are available.
- Sales: The purchases made by the customers.
public class Machine
{
public string Id { get; set; }
public string Location { get; set; }
public string Currency { get; set; }
public string[] PaymentMethods { get; set; }
public int MaxCapacityPerProduct { get; set; }
}
public class Beer
{
public string Id { get; set; }
public string Name { get; set; }
public int Number { get; set; }
public double Price { get; set; }
public int Stock { get; set; }
}
public class Sale
{
public string Id { get; set; }
public DateTime Time { get; set; }
public string BeerId { get; set; }
public int Quantity { get; set; }
public double TotalAmount { get; set; }
}
Step 3: Initializing the Database
Next, we need to set up our database if it doesn’t already exist.
We will do this using the provided GetDocumentStoreAsync(string databaseName) method, which creates the database if it doesn’t exist and returns a store for us to communicate with it.
// Create a document store to communicate with the database
using var store = await EmbeddedServer.Instance.GetDocumentStoreAsync("VendingMachine");
Right now, our database is empty. Let’s initialize our data.
Add a function for creating our products:
public static async Task InitializeProducts(IDocumentStore store)
{
//Create products
using var session = store.OpenAsyncSession();
await session.StoreAsync(new Beer()
{
Name = "Budweiser",
Number = 1,
Price = 2,
Stock = 30,
}, "Budweiser");
await session.StoreAsync(new Beer()
{
Name = "Miller Lite",
Number = 2,
Price = 2.5,
Stock = 30,
}, "Miller-Lite");
await session.StoreAsync(new Beer()
{
Name = "Coors Light",
Number = 3,
Price = 4,
Stock = 30,
}, "Coors-Light");
await session.StoreAsync(new Beer()
{
Name = "Corona Extra",
Number = 4,
Price = 3.5,
Stock = 30,
}, "Corona-Extra");
await session.StoreAsync(new Beer()
{
Name = "Heineken",
Number = 5,
Price = 2,
Stock = 30,
}, "Heineken");
await session.StoreAsync(new Beer()
{
Name = "Blue Moon",
Number = 6,
Price = 2,
Stock = 30,
}, "Blue-Moon");
await session.SaveChangesAsync();
}
Create our Machine document if it doesn’t already exist and call InitializeProducts().
We should also set up a const for a mock machine Id number at the top of the class.
public const string VendingMachineNumber = "411";
// Initialize database data
using (var session = store.OpenAsyncSession())
{
if (await session.LoadAsync<Machine>($"Machines/{VendingMachineNumber}") == null)
{
// Initialize products
await InitializeProducts(store);
// Initialize Machine details
await session.StoreAsync(new Machine()
{
Location = "350 5th Ave, New York, NY 10118, United States",
Currency = "USD",
PaymentMethods = new string[] { "CreditCard", "Cash" },
MaxCapacityPerProduct = 30
},
$"Machines/{VendingMachineNumber}");
await session.SaveChangesAsync();
}
}
Step 4: Load static data into memory
There is some data that can be fetched once on program startup.
Let’s load our Machine Settings that includes the machine’s Currency which we will print out to the user upon purchase.
And a dictionary BeerNumberToId so customers can enter the product number to buy, and we can update its stock immediately via its Id.
// Initialize database data
using (var session = store.OpenAsyncSession())
{
if (await session.LoadAsync<Machine>($"Machines/{VendingMachineNumber}") == null)
{
// Initialize products
await InitializeProducts(store);
// Initialize Machine details
await session.StoreAsync(new Machine()
{
Location = "350 5th Ave, New York, NY 10118, United States",
Currency = "USD",
PaymentMethods = new string[] { "CreditCard", "Cash" },
MaxCapacityPerProduct = 30
},
$"Machines/{VendingMachineNumber}");
await session.SaveChangesAsync();
}
// Load machine settings
Settings = await session.LoadAsync<Machine>($"Machines/{VendingMachineNumber}");
// Load beer Ids and Names
BeerNumberToId =
(await session.Advanced.AsyncDocumentQuery<Beer>().ToListAsync()).ToDictionary(
beerItem => beerItem.Number, beerItem => beerItem.Id);
}
Step 5: Handling Customer Purchases
For every customer, we will print out the current beers, their number, price, and current stock.
So first, let’s update our Beer class to print out the product details with the machine’s currency
public class Beer
{
public string Id { get; set; }
public string Name { get; set; }
public int Number { get; set; }
public double Price { get; set; }
public int Stock { get; set; }
public override string ToString()
{
return $"{Number}. {Name}: {Price} {Program.Settings.Currency}. Remaining: {Stock}";
}
}
Then add this method, which will fetch all beers and list them for the customer
public static async Task PrintStock(IDocumentStore store)
{
using var session = store.OpenAsyncSession();
var beers = await session.Advanced.AsyncDocumentQuery<Beer>().OrderBy(b => b.Number).ToListAsync();
Console.WriteLine();
foreach (var beer in beers)
{
Console.WriteLine(beer);
}
Console.WriteLine();
Console.WriteLine("Enter command: 'buy [Quantity] [BeerNumber]");
}
Since we are building a Console application, we can format our commands this way:
- buy [Quantity] [BeerNumber] – The number of units to buy and the beer’s number
- stock – the machine has been fully restocked
// Start reading user commands
while (true)
{
// Print stock for user
await PrintStock(store);
// Read and process command
var cmd = Console.ReadLine();
if (string.IsNullOrEmpty(cmd))
continue;
var arguments = cmd.Split(' ');
var action = arguments[0];
switch (action.ToLower())
{
case "buy":
{
// Update the stock amount of the product in the database
break;
}
case "stock":
{
// Update all products' stock to be full again
break;
}
default:
{
Console.WriteLine("Unrecognized command");
continue;
}
}
}
Inside buy, we will check the command is valid, update the stock according to the quantity, and record the sale:
case "buy":
{
using var session = store.OpenAsyncSession();
var quantityArg = arguments[1];
var beerNumberArg = arguments[2];
if (int.TryParse(quantityArg, out int quantity) == false)
{
Console.WriteLine($"Quantity must be an integer");
continue;
}
if (int.TryParse(beerNumberArg, out int number) == false || BeerNumberToId.TryGetValue(number, out string beerId) == false)
{
Console.WriteLine($"Beer {beerNumberArg} does not exist.");
continue;
}
// Update stock
var beerDetails = await session.LoadAsync<Beer>(beerId);
if (beerDetails.Stock < quantity)
{
Console.WriteLine($"Not enough {beerDetails.Name} in the machine");
continue;
}
beerDetails.Stock = beerDetails.Stock - quantity;
// Record the sale
await session.StoreAsync(new Sale()
{
BeerId = beerId,
Time = DateTime.UtcNow,
Quantity = quantity,
TotalAmount = beerDetails.Price * quantity
});
await session.SaveChangesAsync();
break;
}
For restocking the machine, we will run a patch operation which will update all beers with MaxCapacityPerProduct.
case "stock":
{
var operation = await store
.Operations
.SendAsync(new PatchByQueryOperation($@"from Beers as b
update
{{
b.Stock = {Settings.MaxCapacityPerProduct};
}}"
));
await operation.WaitForCompletionAsync();
break;
}
Note: Another option is to load the Machine’s document inside the patch code and fetch MaxCapacityPerProduct from it without relying on in-memory data. See this example.
Running our program will give us this screen:
At this point we can go to http://localhost:8080 and view our new data in the studio:
Let’s buy 2 Budweisers
And then restock our machine
We’ll process a couple more sales and then go to view them in the studio:
Let’s look at the stock of Miller Lite
That’s it! We’re done building our vending machine.
Obviously, this is a very simple example, and there are numerous ways in which we can make this program smarter and better using RavenDB’s other features:
- Automatic Price Updates – Replicate product prices and machine settings automatically from the main server, so updating a products price across all machines will only require updating it on the main database.
- Automatic Alerts – Create a subscription that will monitor the stock and create an Alert document whenever a product is out of stock, from there it can be replicated automatically to the main database.
What more can RavenDB’s Embedded Server offer
In conclusion, RavenDB’s embedded server provides a lightweight version of its database server that can be easily incorporated into applications.
This means that developers can harness the full power of RavenDB’s many database features within their applications.
Here are some that may be especially beneficial in embedded servers applications:
- Zero Configuration: With RavenDB’s embedded server, you can simply include the necessary NuGet package and start using it immediately. There’s no need to configure network settings or manage server instances, making it easy to set up many deployments such as:
- Lightweight Microservices: When building microservices that need a local database, using RavenDB’s embedded server can simplify deployment and management of each microservice.
- Replication: One key advantage of RavenDB’s embedded server is its built-in support for replication. This means you can easily replicate data from your embedded database to other instances of RavenDB, whether they are embedded or standalone servers. Replication ensures data consistency and availability across multiple instances of your application and exists in more forms such as hub/sink and filtered replications. This feature is particularly useful for applications that need to synchronize data between client devices and a central server, such as:
- Remote Applications: In scenarios where you have remote applications running on user machines, each application can have its own embedded RavenDB instance. This allows each instance to have its own local data store and replicate only the relevant data using a filtered replication.
- Internet of Things (IoT) Devices: Embedded servers play a crucial role in IoT devices, enabling them to communicate with each other and with cloud services.
- Offline Support: For applications that need to work offline and sync data when online, an embedded RavenDB can store data locally and sync with a central server when connectivity is available.
- Conflict Resolution: When using replication with RavenDB, it includes conflict resolution strategies that can automatically handle conflicts that arise when data is updated simultaneously on different replicas. You can define rules to resolve conflicts based on timestamps, document revisions, or custom logic, ensuring data integrity across replicated databases.
- Booking and Reservation Systems: In booking systems for hotels, airlines, or events, conflicts can arise if two users reserve the same resource simultaneously. RavenDB’s conflict resolution ensures fair handling of such conflicts, preventing double bookings or overbooking situations.
- Offline-First Applications: Applications designed to work offline and sync with a central server when online face challenges when conflicts arise between local and server data. For instance, in an app used by field workers to update inventory, conflicts may occur if two users modify the same item offline. RavenDB’s conflict resolution mechanisms can resolve such conflicts during synchronization, preventing data loss or corruption.
- Subscription: RavenDB’s embedded server also supports subscriptions, which allow your application to receive real-time updates about changes in the database. Subscriptions provide a way to efficiently monitor changes without continuously polling the database. This is especially valuable for applications that require real-time data updates, such as chat applications, notifications, or live feeds. Subscriptions can be configured to filter specific documents or changes, reducing unnecessary network traffic and improving application responsiveness.
- Real-Time Dashboards: Analysts rely on up-to-the-minute data to make informed decisions. RavenDB’s subscriptions continuously notify the dashboard components of any changes in underlying data, ensuring that the graphs, charts, and metrics are always current.
- Messaging and Chat Applications: Subscriptions can notify users of new messages, updates, or other activities in real-time. This ensures that users are promptly informed of incoming messages or changes in conversation status without delay.
- Automated Alerts: In systems that require instant notifications or alerts based on certain events, subscription’s benefits can include notifying users of critical system updates, new orders in an e-commerce platform, or changes to important data points.
- High Performance: RavenDB is known for its high performance and efficient use of system resources. The embedded server benefits from these performance optimizations, providing fast read and write operations even when running within the same process as your application. This high performance is crucial for applications that require low latency and high throughput, such as real-time analytics, IoT data processing, as well as:
- Embedded Analytics: If your application needs to perform analytics on its data, having an embedded RavenDB instance allows for efficient data retrieval and analysis without the need to send data over the network.
- Industrial Applications: Embedded servers are prevalent in industrial machines and equipment, where they can provide monitoring, control, and data processing capabilities.
- Single-User Desktop Applications: Applications that need a local database for storing user-specific data, such as desktop applications, can benefit from RavenDB’s processing capabilities.
- ACID Transactions: RavenDB’s embedded server supports ACID (Atomicity, Consistency, Isolation, Durability) transactions, ensuring data integrity and reliability. Transactions allow you to group multiple database operations into a single unit of work, ensuring that either all operations succeed or none of them are applied. This is critical for maintaining data consistency, especially in complex applications with multiple concurrent operations.
- Gaming and Real-Time Applications: Real-time applications, including online gaming platforms, benefit from ACID transactions to manage in-game purchases, player interactions, and game state changes. ACID guarantees that when a player buys an item, makes progress, or interacts with other players, these actions are executed reliably and consistently.
- Financial Applications: For financial applications handling transactions, ACID compliance is non-negotiable. Whether it’s processing withdrawals, deposits, or transfers, ACID transactions ensure that these operations are atomic, preventing incomplete or erroneous financial operations.
- Collaborative Editing Tools: For collaborative editing tools like Google Docs or Microsoft Office Online, ACID transactions ensure that changes made by multiple users are synchronized correctly. Each edit operation is treated as a transaction, allowing for seamless merging of changes while maintaining the integrity of the document.
You can browse RavenDB’s entire feature set on this page.
Some of RavenDB’s features are license-dependent. Please reference Step 1 on how to apply a license.
Woah, already finished? 🤯
If you found the article interesting, don’t miss a chance to try our database solution – totally for free!