Handle Time Series Data with MongoDB
Tim Kelly13 min read • Published Nov 19, 2024 • Updated Nov 19, 2024
FULL APPLICATION
Rate this tutorial
While relational databases are the common default for many tasks, MongoDB is uniquely equipped to handle time series applications—from how the data is stored and accessed, to how your database is expected to scale. In this tutorial, we’ll build a ship-tracking application to manage a fleet of boats traversing the Atlantic. Picture the sky filled with airplanes, packs of Amazon delivery trucks on the roads, or schools of Uber cars lurking around a bustling downtown on a Saturday night. All of these scenarios generate massive amounts of time series data as each vehicle's location is continuously tracked and sent to databases. The full application is available on GitHub. So let’s get to work and see why MongoDB is the best database for handling time series data.
Large clients, such as those in finance, IoT, or telecommunications, manage massive volumes of time series data. High write performance is essential to handle continuous data influx efficiently without bottlenecks or data loss.
The typical spheres in which we utilize time series data are the likes of finance, IoT, and telecommunications. These are all domains that create a massive, continuous influx of data that need to be handled without bottlenecks or data loss. MongoDB has a native time series collection specifically optimized for high write throughput.
Time series collections use an underlying columnar storage format and store data in time-order. This format provides improved query efficiency, reduced disk usage, reduced I/O for read operations, and increased WiredTiger cache usage.
Now let’s pit them against each other: MongoDB vs relational databases! Doing all this with relational databases is possible but often needs tuning, such as table partitioning and using extensions, or a dedicated time series database, to match MongoDB's write performance. This requires more setup and ongoing maintenance.
Sectors such as finance, logistics, and cloud services face ever-growing demands for database scalability. As data volumes expand, these clients need to scale their databases horizontally to maintain performance and meet user demands efficiently.
In MongoDB, horizontal scaling is made simple through native sharding. Sharding enables the distribution of data across multiple servers or nodes, so that no single server becomes a bottleneck. This offers high availability, increased read and write throughput, and practically unlimited storage capacity. We can always add another computer!
Sharding is vital for maintaining high performance as data scales. In contrast, a popular database like PostgreSQL relies on vertical scaling by default and may require complex external solutions like Citus for horizontal scaling. These solutions add to setup time and operational complexity, making MongoDB's integrated approach more straightforward and attractive to clients with large-scale requirements.
To follow along with this tutorial, you should have the following:
Create a Maven application, and go to the POM. Here we'll add our dependencies.
1 <dependencies> 2 <dependency> 3 <groupId>org.mongodb</groupId> 4 <artifactId>mongodb-driver-sync</artifactId> 5 <version>5.2.0</version> 6 </dependency> 7 <dependency> 8 <groupId>io.javalin</groupId> 9 <artifactId>javalin</artifactId> 10 <version>6.3.0</version> 11 </dependency> 12 <dependency> 13 <groupId>com.fasterxml.jackson.core</groupId> 14 <artifactId>jackson-databind</artifactId> 15 <version>2.17.2</version> 16 </dependency> 17 </dependencies>
We are using the MongoDB Java Driver to access our interaction with our database. Next, we have Javalin to create our API. Lastly, we are using Jackson Databind to, yes, bind our data.
Create a class MongodbConfig.java in a Config package to connect to MongoDB and ensure the database and collection exist. We need to initialize our database and create our time series collection.
1 package com.mongodb.shiptracker.config; 2 3 import com.mongodb.ConnectionString; 4 import com.mongodb.MongoClientSettings; 5 import com.mongodb.client.MongoClient; 6 import com.mongodb.client.MongoClients; 7 import com.mongodb.client.MongoDatabase; 8 import com.mongodb.ServerApi; 9 import com.mongodb.ServerApiVersion; 10 import com.mongodb.client.MongoIterable; 11 import com.mongodb.client.model.CreateCollectionOptions; 12 import com.mongodb.client.model.TimeSeriesGranularity; 13 import com.mongodb.client.model.TimeSeriesOptions; 14 15 public class MongodbConfig { 16 private static final String CONNECTION_STRING = "YOUR-CONNECTION-STRING"; 17 private static MongoDatabase database; 18 19 public static MongoDatabase getDatabase() { 20 if (database == null) { 21 database = initializeDatabase(); 22 } 23 createTimeSeriesCollectionIfNotExists(database, "shipLocations"); 24 25 return database; 26 } 27 28 private static MongoDatabase initializeDatabase() { 29 ServerApi serverApi = ServerApi.builder() 30 .version(ServerApiVersion.V1) 31 .build(); 32 MongoClientSettings settings = MongoClientSettings.builder() 33 .applyConnectionString(new ConnectionString(CONNECTION_STRING)) 34 .serverApi(serverApi) 35 .build(); 36 37 MongoClient mongoClient = MongoClients.create(settings); 38 MongoDatabase db = mongoClient.getDatabase("ShipTracker"); 39 40 return db; 41 } 42 43 private static void createTimeSeriesCollectionIfNotExists(MongoDatabase db, String collectionName) { 44 45 MongoIterable<String> collections = db.listCollectionNames(); 46 for (String name : collections) { 47 if (name.equals(collectionName)) { 48 System.out.println("Time series collection '" + collectionName + "' already exists."); 49 return; 50 } 51 } 52 53 TimeSeriesOptions timeSeriesOptions = new TimeSeriesOptions("timestamp") 54 .metaField("boatId") 55 .granularity(TimeSeriesGranularity.SECONDS); 56 57 CreateCollectionOptions collOptions = new CreateCollectionOptions().timeSeriesOptions(timeSeriesOptions); 58 59 db.createCollection(collectionName, collOptions); 60 System.out.println("Time series collection '" + collectionName + "' created."); 61 } 62 }
For our time series collection, we specify a
timeField
as a field to contain our time data—timestamp, in our case. This must be a date type.We then add our
metaField
. Time series documents can contain metadata about each document. MongoDB uses the metaField
to group sets of documents, both for internal storage optimization and query efficiency. Check out our docs to learn more about the metaField at metaField Considerations.Lastly, we have our granularity. When we create a time series collection, MongoDB automatically creates a system.buckets system collection and groups incoming time series data into buckets. By setting granularity, we control how frequently data is bucketed based on the ingestion rate of your data.
You can use the custom bucketing parameters bucketMaxSpanSeconds and bucketRoundingSeconds to specify bucket boundaries and more accurately control how time series data is bucketed.
We are going to use seconds, as this is just a simulation of our boats traveling over that big blue sea, and it would be more than tedious to wait for our hourly updates.
Now, we need some models to interact with our application. Create a basic model for ports to represent their geographical locations. Create a class
Port
and Location
in a model package.1 package com.mongodb.shiptracker.model; 2 3 import org.bson.Document; 4 5 import java.util.Arrays; 6 7 public class Port { 8 private String name; 9 private double latitude; 10 private double longitude; 11 12 public Port(String name, double latitude, double longitude) { 13 this.name = name; 14 this.latitude = latitude; 15 this.longitude = longitude; 16 } 17 18 public Document toDocument() { 19 return new Document("name", name) 20 .append("location", new Document("type", "Point") 21 .append("coordinates", Arrays.asList(longitude, latitude))); // GeoJSON format: [longitude, latitude] 22 } 23 24 // add your getters and setters 25 }
1 package com.mongodb.shiptracker.model; 2 3 public class Location { 4 private double latitude; 5 private double longitude; 6 7 public Location(double latitude, double longitude) { 8 this.latitude = latitude; 9 this.longitude = longitude; 10 } 11 12 // add your getters and setters 13 }
We're adding a toDocument method at the bottom so we can store our ports as GeoJson data. This will allow us to take advantage of MongoDBs GeoSpatial Queries.
For this example, we'll be tracking boats along the open sea. To do this, we'll first need a
Boat
model.1 package com.mongodb.shiptracker.model; 2 3 import org.bson.Document; 4 import org.bson.types.ObjectId; 5 6 public class Boat { 7 private ObjectId _id; 8 private String boatId; 9 private Location location; 10 private Location destination; 11 private String startPort; 12 private String endPort; 13 14 public Boat () {} 15 16 public Boat(String boatId, Location location, Location destination) { 17 this._id = new ObjectId(); 18 this.boatId = boatId; 19 this.location = location; 20 this.destination = destination; 21 } 22 23 public String getStartPort() { 24 return startPort; 25 } 26 27 public void setStartPort(String startPort) { 28 this.startPort = startPort; 29 } 30 31 public String getEndPort() { 32 return endPort; 33 } 34 35 public void setEndPort(String endPort) { 36 this.endPort = endPort; 37 } 38 39 public Boat(String boatId, String startPort, String endPort) { 40 this._id = new ObjectId(); 41 this.boatId = boatId; 42 this.startPort = startPort; 43 this.endPort = endPort; 44 } 45 46 // add your getters and setters 47 48 public Document toDocument() { 49 return new Document("_id", _id) 50 .append("boatId", boatId) 51 .append("startLocation", new Document("latitude", location.getLatitude()) 52 .append("longitude", location.getLongitude())) 53 .append("destination", new Document("latitude", destination.getLatitude()) 54 .append("longitude", destination.getLongitude())); 55 } 56 }
As before, we are adding a
toDocument
method, but that's not all. We need our boats to move across that deep blue sea. Add a method move to the bottom of your boat model.1 // Simulate movement by moving the boat incrementally towards its destination 2 public void move() { 3 double currentLatitude = location.getLatitude(); 4 double currentLongitude = location.getLongitude(); 5 double destLatitude = destination.getLatitude(); 6 double destLongitude = destination.getLongitude(); 7 8 double nauticalMilesPerHour = 20; 9 double stepSizeInDegrees = nauticalMilesPerHour / 60.0; // Convert nautical miles to degrees 10 11 // Calculate direction double latDirection = destLatitude - currentLatitude; 12 double lonDirection = destLongitude - currentLongitude; 13 double distance = Math.sqrt(latDirection * latDirection + lonDirection * lonDirection); 14 15 if (distance > stepSizeInDegrees) { 16 // Normalize direction 17 latDirection /= distance; 18 lonDirection /= distance; 19 20 // Update current location based on step size 21 double newLatitude = currentLatitude + latDirection * stepSizeInDegrees; 22 double newLongitude = currentLongitude + lonDirection * stepSizeInDegrees; 23 24 location.setLatitude(newLatitude); 25 location.setLongitude(newLongitude); 26 } else { 27 // If the boat is close enough, set it directly to the destination 28 location.setLatitude(destLatitude); 29 location.setLongitude(destLongitude); 30 } 31 }
Now, how do we request a boat? For the ease of interfacing with our API to be, and mapping our JSON inputs, we will have a boat request.
1 package com.mongodb.shiptracker.model; 2 3 public class BoatRequest { 4 private String boatId; 5 private String startPort; 6 private String endPort; 7 8 public BoatRequest() { 9 } 10 11 // getters and setters 12 }
This will allow us to provide a boat ID, and a start and destination port, from the list of ports in the database.
Oh the task of interfacing with a database. This will reside in our repository package, across several repos and implementations. Let's start with the port repo.
Create a
PortRepository
.1 package com.mongodb.shiptracker.repository; 2 3 import com.mongodb.shiptracker.model.Port; 4 5 public interface PortRepository { 6 void addPort(Port port); 7 Port getPortByName(String name); 8 }
Here we just need to add a port and get the coordinates of a port, given its name. Now, for the
PortRepositoryImpl
:1 package com.mongodb.shiptracker.repository; 2 3 import com.mongodb.client.MongoCollection; 4 import com.mongodb.client.MongoDatabase; 5 import com.mongodb.shiptracker.config.MongodbConfig; 6 import com.mongodb.shiptracker.model.Port; 7 import org.bson.Document; 8 9 import java.util.List; 10 11 public class PortRepositoryImpl implements PortRepository { 12 private final MongoDatabase database; 13 14 public PortRepositoryImpl() { 15 this.database = MongodbConfig.getDatabase(); 16 } 17 18 19 public void addPort(Port port) { 20 MongoCollection<Document> collection = database.getCollection("ports"); 21 collection.insertOne(port.toDocument()); 22 } 23 24 25 public Port getPortByName(String name) { 26 MongoCollection<Document> collection = database.getCollection("ports"); 27 Document query = new Document("name", name); 28 Document result = collection.find(query).first(); 29 30 if (result != null) { 31 List<Double> coordinates = result.get("location", Document.class).getList("coordinates", Double.class); 32 return new Port( 33 result.getString("name"), 34 coordinates.get(1), // Latitude 35 coordinates.get(0) // Longitude 36 ); 37 } 38 return null; 39 } 40 }
A very minimalistic error handling, but plenty to get us up and running.
Now, we'll create our even simpler
BoatRepository
:1 package com.mongodb.shiptracker.repository; 2 3 import com.mongodb.shiptracker.model.Boat; 4 5 public interface BoatRepository { 6 void addBoat(Boat boat); 7 }
And we'll implement our lone method:
1 package com.mongodb.shiptracker.repository; 2 3 import com.mongodb.client.MongoCollection; 4 import com.mongodb.client.MongoDatabase; 5 import com.mongodb.shiptracker.config.MongodbConfig; 6 import com.mongodb.shiptracker.model.Boat; 7 import org.bson.Document; 8 9 10 public class BoatRepositoryImpl implements BoatRepository { 11 private final MongoDatabase database; 12 13 public BoatRepositoryImpl() { 14 this.database = MongodbConfig.getDatabase(); 15 } 16 17 18 public void addBoat(Boat boat) { 19 MongoCollection<Document> collection = database.getCollection("boats"); 20 collection.insertOne(boat.toDocument()); 21 } 22 }
Oh so simple. All we are doing here is adding a method to add a boat to our database.
Now, we will have some more fun here. Let's create a
TimeSeriesRepository
and add a method to take advantage of MongoDB's ability to handle geospatial queries in a time series collection.1 package com.mongodb.shiptracker.repository; 2 3 import com.mongodb.shiptracker.model.Location; 4 import org.bson.Document; 5 6 import java.util.List; 7 8 public interface TimeSeriesRepository { 9 void logBoatLocation(String boatId, Location location); 10 List<Document> calculateTotalDistanceTraveled(); 11 }
We'll have a method to calculate the total distance traveled by all the boats currently traveling in our application.
1 package com.mongodb.shiptracker.repository; 2 3 import com.mongodb.client.AggregateIterable; 4 import com.mongodb.client.MongoCollection; 5 import com.mongodb.client.MongoDatabase; 6 import com.mongodb.client.model.*; 7 import com.mongodb.shiptracker.config.MongodbConfig; 8 import com.mongodb.shiptracker.model.Location; 9 import org.bson.Document; 10 11 import java.util.ArrayList; 12 import java.util.Arrays; 13 import java.util.Date; 14 import java.util.List; 15 16 17 public class TimeSeriesRepositoryImpl implements TimeSeriesRepository { 18 19 private final MongoDatabase database; 20 21 public TimeSeriesRepositoryImpl() { 22 this.database = MongodbConfig.getDatabase(); 23 } 24 25 26 public void logBoatLocation(String boatId, Location location) { 27 MongoCollection<Document> collection = database.getCollection("shipLocations"); 28 Document geoJsonLocation = new Document("type", "Point") 29 .append("coordinates", Arrays.asList(location.getLongitude(), location.getLatitude())); 30 31 Document logEntry = new Document("boatId", boatId) 32 .append("timestamp", new Date()) 33 .append("location", geoJsonLocation); 34 35 collection.insertOne(logEntry); 36 } 37 38 39 public List<Document> calculateTotalDistanceTraveled() { 40 MongoCollection<Document> collection = database.getCollection("shipLocations"); 41 42 // Step 1: Set window fields to shift coordinates for calculating the previous position 43 Document setWindowFieldsStage = new Document("$setWindowFields", 44 new Document("partitionBy", "$boatId") 45 .append("sortBy", new Document("timestamp", 1L)) 46 .append("output", 47 new Document("previousCoordinates", 48 new Document("$shift", 49 new Document("output", "$location.coordinates") 50 .append("by", -1L) 51 ) 52 ) 53 ) 54 ); 55 56 // Step 2: Calculate the distance between current and previous coordinates 57 Document setDistanceStage = new Document("$set", 58 new Document("distance", 59 new Document("$sqrt", 60 new Document("$add", Arrays.asList( 61 new Document("$pow", Arrays.asList( 62 new Document("$subtract", Arrays.asList( 63 new Document("$arrayElemAt", Arrays.asList("$location.coordinates", 1L)), 64 new Document("$arrayElemAt", Arrays.asList("$previousCoordinates", 1L)) 65 )), 66 2L 67 )), 68 new Document("$pow", Arrays.asList( 69 new Document("$subtract", Arrays.asList( 70 new Document("$arrayElemAt", Arrays.asList("$location.coordinates", 0L)), 71 new Document("$arrayElemAt", Arrays.asList("$previousCoordinates", 0L)) 72 )), 73 2L 74 )) 75 )) 76 ) 77 ) 78 ); 79 80 // Step 3: Group by boatId and calculate the total distance 81 Document groupTotalDistanceStage = new Document("$group", 82 new Document("_id", "$boatId") 83 .append("totalDistance", new Document("$sum", "$distance")) 84 ); 85 86 // Perform the aggregation and collect results into a list 87 AggregateIterable<Document> result = collection.aggregate(Arrays.asList(setWindowFieldsStage, setDistanceStage, groupTotalDistanceStage)); 88 List<Document> resultList = new ArrayList<>(); 89 result.forEach(resultList::add); 90 91 return resultList; 92 } 93 94 }
While this aggregation might not be perfectly optimized, it does show some cool features. MongoDB is a time series database, when implemented with a time series collection. It provides aggregation pipeline stages specifically there for analyzing time series data, such as $setWindowFields, which we can use to apply one or more operators and a specific time frame in our data.
The first step figures out where each boat was just before the current record:
It groups the data by boat ID and then sorts the data by time, so the most recent positions are in order. For each record, it adds a new field called
It groups the data by boat ID and then sorts the data by time, so the most recent positions are in order. For each record, it adds a new field called
previousCoordinates
, which contains the GPS position from the previous timestamp.This is like saying, "For each position, what was the boat's last position?"
Now that each record knows both the current and previous positions, it calculates how far the boat traveled between those two points:
Now that each record knows both the current and previous positions, it calculates how far the boat traveled between those two points:
The formula it uses is the Pythagorean theorem. It goes a little like this:
- Find the difference between the current and previous latitude.
- Find the difference between the current and previous longitude.
- Square both differences and add them together.
- Take the square root of the sum to get the distance.
This adds a new field to each record called
distance
, which is how far the boat moved between the two positions.Finally, it calculates the total distance each boat has traveled: It groups all the records for each boat. Lastly, it adds up all the distance values for each boat to get the total distance traveled.
Read more about what is available with time series data and see what else is possible with the aggregation stages.
Now that we have the repositories created and implemented, it's time to get down to business (logic).
Let's start with the boat service. Create a service package, and we'll add our
BoatService
there.1 package com.mongodb.shiptracker.service; 2 3 import com.mongodb.shiptracker.model.Boat; 4 import com.mongodb.shiptracker.model.Location; 5 import com.mongodb.shiptracker.model.Port; 6 import com.mongodb.shiptracker.repository.BoatRepositoryImpl; 7 import com.mongodb.shiptracker.repository.PortRepositoryImpl; 8 9 public class BoatService { 10 private final PortRepositoryImpl portRepository; 11 private final BoatRepositoryImpl boatRepository; 12 13 public BoatService() { 14 this.portRepository = new PortRepositoryImpl(); 15 this.boatRepository = new BoatRepositoryImpl(); 16 } 17 18 public Boat createBoat(String boatId, String startPortName, String endPortName) { 19 Port startPort = portRepository.getPortByName(startPortName); 20 Port endPort = portRepository.getPortByName(endPortName); 21 22 if (startPort != null && endPort != null) { 23 Boat boat = new Boat( 24 boatId, 25 new Location(startPort.getLatitude(), startPort.getLongitude()), 26 new Location(endPort.getLatitude(), endPort.getLongitude()) 27 ); 28 boatRepository.addBoat(boat); 29 return boat; 30 } else { 31 return null; 32 } 33 } 34 }
Here, we'll create our boats. We take in the start and end port names, and we get the coordinates we need to initialize our boat location and intended destination.
Next, we have our
PortService
.1 package com.mongodb.shiptracker.service; 2 3 import com.mongodb.shiptracker.model.Port; 4 import com.mongodb.shiptracker.repository.PortRepositoryImpl; 5 6 import java.util.Arrays; 7 import java.util.List; 8 9 public class PortService { 10 private final PortRepositoryImpl portRepository; 11 12 public PortService() { 13 this.portRepository = new PortRepositoryImpl(); 14 } 15 16 public void insertInitialPorts() { 17 List<Port> ports = Arrays.asList( 18 new Port("New York", 40.7128, -74.0060), 19 new Port("Rotterdam", 51.9244, 4.4777), 20 new Port("Savannah", 32.0835, -81.0998), 21 new Port("Antwerp", 51.2194, 4.4025), 22 new Port("Miami", 25.7617, -80.1918), 23 new Port("Lisbon", 38.7223, -9.1393), 24 new Port("Halifax", 44.6488, -63.5752), 25 new Port("Le Havre", 49.4944, 0.1079), 26 new Port("Charleston", 32.7765, -79.9311), 27 new Port("Hamburg", 53.5511, 9.9937) 28 ); 29 ports.forEach(portRepository::addPort); 30 } 31 }
This will initialize our application with a list of ports around the world when called. Now, on careful inspection, you may notice this isn't the exact location of these ports, but in my defense, that is harder information to find than you might expect. It will do for our demo (ignore the start and end of each journey where the boat miraculously traverses land).
Now, we have a very simple
DistanceService
.1 package com.mongodb.shiptracker.service; 2 3 import com.mongodb.shiptracker.repository.TimeSeriesRepositoryImpl; 4 import org.bson.Document; 5 6 import java.util.List; 7 8 public class DistanceService { 9 private final TimeSeriesRepositoryImpl timeSeriesRepository; 10 11 public DistanceService() { 12 this.timeSeriesRepository = new TimeSeriesRepositoryImpl(); 13 } 14 15 public List<Document> calculateTotalDistanceTraveled() { 16 return timeSeriesRepository.calculateTotalDistanceTraveled(); 17 } 18 }
To get things running without geotagging a bunch of real ships navigating the open ocean (my manager denied the budget request), let's create a quick simulation class.
1 package com.mongodb.shiptracker.service; 2 3 import com.mongodb.shiptracker.model.Boat; 4 import com.mongodb.shiptracker.repository.TimeSeriesRepositoryImpl; 5 6 import java.util.ArrayList; 7 import java.util.List; 8 9 public class Simulator { 10 private final List<Boat> boats; 11 private final TimeSeriesRepositoryImpl timeSeriesRepositoryImpl; 12 13 public Simulator() { 14 this.boats = new ArrayList<>(); 15 this.timeSeriesRepositoryImpl = new TimeSeriesRepositoryImpl(); // Initialize the time series repository 16 } 17 18 public void addBoat(Boat boat) { 19 boats.add(boat); 20 } 21 22 // Run the simulation until each boat reaches its destination 23 public void runSimulation() { 24 boolean hasActiveBoats = true; 25 26 while (hasActiveBoats) { 27 System.out.println("Simulation step"); 28 hasActiveBoats = false; 29 30 // Move all boats in one step and log their locations 31 for (Boat boat : new ArrayList<>(boats)) { 32 if (!(boat.getLocation().getLatitude() == boat.getDestination().getLatitude() && 33 boat.getLocation().getLongitude() == boat.getDestination().getLongitude())) { 34 boat.move(); 35 timeSeriesRepositoryImpl.logBoatLocation(boat.getBoatId(), boat.getLocation()); // Log the new location 36 37 System.out.println("Boat " + boat.getBoatId() + " moved to new position: (" + 38 boat.getLocation().getLatitude() + ", " + boat.getLocation().getLongitude() + ")"); 39 40 hasActiveBoats = true; 41 } else { 42 System.out.println("Boat " + boat.getBoatId() + " has reached its destination."); 43 } 44 } 45 46 // Remove boats that have reached their destinations after all have been processed 47 boats.removeIf(boat -> boat.getLocation().getLatitude() == boat.getDestination().getLatitude() && 48 boat.getLocation().getLongitude() == boat.getDestination().getLongitude()); 49 50 try { 51 Thread.sleep(1000); // Pause for 1 second between steps to simulate real-time updates 52 } catch (InterruptedException e) { 53 e.printStackTrace(); 54 } 55 } 56 System.out.println("All boats have reached their destinations. Simulation complete."); 57 } 58 }
This will allow us to simulate the boats in our databases moving to their destination. Nothing fancy—we are expecting the boats to move in a straight line and a steady speed, and update the location each second.
To interact with our app, we are going to use Javalin to create a simple API. First, we need a controller package, where we can add our
ShipTrackerController
.1 package com.mongodb.shiptracker.controller; 2 3 import com.mongodb.shiptracker.model.Boat; 4 import com.mongodb.shiptracker.model.BoatRequest; 5 import com.mongodb.shiptracker.service.BoatService; 6 import com.mongodb.shiptracker.service.DistanceService; 7 import com.mongodb.shiptracker.service.PortService; 8 import com.mongodb.shiptracker.service.Simulator; 9 import io.javalin.Javalin; 10 import io.javalin.http.Context; 11 12 public class ShipTrackerController { 13 private final PortService portService; 14 private final BoatService boatService; 15 private final Simulator simulator; 16 private final DistanceService distanceService; 17 18 public ShipTrackerController() { 19 this.portService = new PortService(); 20 this.boatService = new BoatService(); 21 this.simulator = new Simulator(); 22 this.distanceService = new DistanceService(); 23 } 24 25 public void registerRoutes(Javalin app) { 26 app.post("/ports/init", this::insertInitialPorts); // Initializes ports 27 app.post("/boats", this::createBoat); // Creates a boat 28 app.post("/simulate", this::runSimulation); // Runs the simulation 29 app.get("/boats/totalDistance", this::getTotalDistances); // Gets total distance traveled for all boats 30 31 app.exception(Exception.class, (e, ctx) -> { 32 e.printStackTrace(); // Logs the full stack trace for debugging 33 ctx.status(500).result("Internal Server Error: " + e.getMessage()); 34 }); 35 } 36 37 private void insertInitialPorts(Context ctx) { 38 portService.insertInitialPorts(); 39 ctx.status(201).result("Ports initialized."); 40 } 41 42 private void createBoat(Context ctx) { 43 System.out.println("Received JSON: " + ctx.body()); // Log raw JSON for verification 44 45 BoatRequest boatRequest = ctx.bodyAsClass(BoatRequest.class); 46 47 System.out.println(boatRequest.getStartPort()); 48 49 if (boatRequest.getBoatId() == null) { 50 ctx.status(400).result("Missing 'boatId' parameter."); 51 return; 52 } 53 if (boatRequest.getStartPort() == null) { 54 ctx.status(400).result("Missing 'startPort' parameter."); 55 return; 56 } 57 if (boatRequest.getEndPort() == null) { 58 ctx.status(400).result("Missing 'endPort' parameter."); 59 return; 60 } 61 62 Boat boat = boatService.createBoat(boatRequest.getBoatId(), boatRequest.getStartPort(), boatRequest.getEndPort()); 63 if (boat != null) { 64 simulator.addBoat(boat); 65 ctx.status(201).json(boat); 66 } else { 67 ctx.status(404).result("One or both ports not found."); 68 } 69 } 70 71 private void runSimulation(Context ctx) { 72 simulator.runSimulation(); 73 ctx.status(200).result("Simulation complete."); 74 } 75 76 private void getTotalDistances(Context ctx) { 77 ctx.json(distanceService.calculateTotalDistanceTraveled()); 78 } 79 }
This will allow us to access all that logic we've been working on until now. We can initialize our ports, add our boat(s), simulate our boats moving, and get the total distance traveled by all boats.
Now, we need a
ShipTrackerApp
for our project.1 package com.mongodb.shiptracker; 2 3 import com.mongodb.shiptracker.controller.ShipTrackerController; 4 import io.javalin.Javalin; 5 6 public class ShipTrackerApp { 7 public static void main(String[] args) { 8 Javalin app = Javalin.create().start(7070); 9 10 ShipTrackerController controller = new ShipTrackerController(); 11 controller.registerRoutes(app); 12 } 13 }
Here we create our Javalin app, and run it on port 7070.
Where the real fun begins. Let's compile and run our project with the following:
1 mvn clean install 2 mvn exec:java -Dexec.mainClass="com.mongodb.shiptracker.ShipTrackerApp"
Now, we can initialize our ports.
1 curl -X POST http://localhost:7070/ports/init
With our ports added, let's make sure our boat’s endpoint is working.
1 curl -X POST http://localhost:7070/boats -H "Content-Type: application/json" -d '{"boatId": "BOAT001","startPort": "Miami","endPort": "Lisbon"}'
What's better than one boat? Two boats!
1 curl -X POST http://localhost:7070/boats -H "Content-Type: application/json" -d '{"boatId": "BOAT002","startPort": "New York","endPort": "Lisbon"}'
Now that we have something to track, let's run our simulation.
1 curl -X POST http://localhost:7070/simulate
In our terminal, we should see the boats moving along.
1 Simulation step 2 Boat BOAT001 moved to new position: (25.821515943344753, -79.86387750300888) 3 Boat BOAT002 moved to new position: (40.70257614347035, -73.67282349442259)
And if we want to run our aggregation:
1 curl -X GET http://localhost:7070/boats/totalDistance
We should get something like this:
1 [{"_id":"BOAT001","totalDistance":230.4497823966521},{"_id":"BOAT002","totalDistance":21.333333333333417}]
Woohoo!
In this tutorial, we built a ship tracker simulation to demonstrate how to create and use time series collections in MongoDB. We created the time series collection with the Java driver and performed aggregations that made use of both the features of time series data and GeoJson.
If you found this tutorial useful, make sure to check out more of our articles on using MongoDB with Java on the Developer Center. Check out how to build a playlist crafted by AI, using Deeplearning4j.
Top Comments in Forums
There are no comments on this article yet.