Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
MongoDB
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
MongoDBchevron-right

Handle Time Series Data with MongoDB

Tim Kelly13 min read • Published Nov 19, 2024 • Updated Nov 19, 2024
MongoDBTime seriesJava
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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.
How do I choose a time series database?
High write throughput and performance
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.
Scaling and sharding
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.
Prerequisites
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.
Connecting to MongoDB
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.
1package com.mongodb.shiptracker.config;
2
3import com.mongodb.ConnectionString;
4import com.mongodb.MongoClientSettings;
5import com.mongodb.client.MongoClient;
6import com.mongodb.client.MongoClients;
7import com.mongodb.client.MongoDatabase;
8import com.mongodb.ServerApi;
9import com.mongodb.ServerApiVersion;
10import com.mongodb.client.MongoIterable;
11import com.mongodb.client.model.CreateCollectionOptions;
12import com.mongodb.client.model.TimeSeriesGranularity;
13import com.mongodb.client.model.TimeSeriesOptions;
14
15public 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}
Add your own connection string for your database, which you can get from the Atlas UI.
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.
Models for our data
A simple port (and location) model
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.
1package com.mongodb.shiptracker.model;
2
3import org.bson.Document;
4
5import java.util.Arrays;
6
7public 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}
1package com.mongodb.shiptracker.model;
2
3public 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.
We're gonna need a boat (any size will do)
For this example, we'll be tracking boats along the open sea. To do this, we'll first need a Boat model.
1package com.mongodb.shiptracker.model;
2
3import org.bson.Document;
4import org.bson.types.ObjectId;
5
6public 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.
1package com.mongodb.shiptracker.model;
2
3public 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.
Repositories and implementations
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.
Port repository
Create a PortRepository.
1package com.mongodb.shiptracker.repository;
2
3import com.mongodb.shiptracker.model.Port;
4
5public 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:
1package com.mongodb.shiptracker.repository;
2
3import com.mongodb.client.MongoCollection;
4import com.mongodb.client.MongoDatabase;
5import com.mongodb.shiptracker.config.MongodbConfig;
6import com.mongodb.shiptracker.model.Port;
7import org.bson.Document;
8
9import java.util.List;
10
11public class PortRepositoryImpl implements PortRepository {
12 private final MongoDatabase database;
13
14 public PortRepositoryImpl() {
15 this.database = MongodbConfig.getDatabase();
16 }
17
18 @Override
19 public void addPort(Port port) {
20 MongoCollection<Document> collection = database.getCollection("ports");
21 collection.insertOne(port.toDocument());
22 }
23
24 @Override
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.
Boat repository
Now, we'll create our even simpler BoatRepository:
1package com.mongodb.shiptracker.repository;
2
3import com.mongodb.shiptracker.model.Boat;
4
5public interface BoatRepository {
6 void addBoat(Boat boat);
7}
And we'll implement our lone method:
1package com.mongodb.shiptracker.repository;
2
3import com.mongodb.client.MongoCollection;
4import com.mongodb.client.MongoDatabase;
5import com.mongodb.shiptracker.config.MongodbConfig;
6import com.mongodb.shiptracker.model.Boat;
7import org.bson.Document;
8
9
10public class BoatRepositoryImpl implements BoatRepository {
11 private final MongoDatabase database;
12
13 public BoatRepositoryImpl() {
14 this.database = MongodbConfig.getDatabase();
15 }
16
17 @Override
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.
TimeSeries repository
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.
1package com.mongodb.shiptracker.repository;
2
3import com.mongodb.shiptracker.model.Location;
4import org.bson.Document;
5
6import java.util.List;
7
8public 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.
1package com.mongodb.shiptracker.repository;
2
3import com.mongodb.client.AggregateIterable;
4import com.mongodb.client.MongoCollection;
5import com.mongodb.client.MongoDatabase;
6import com.mongodb.client.model.*;
7import com.mongodb.shiptracker.config.MongodbConfig;
8import com.mongodb.shiptracker.model.Location;
9import org.bson.Document;
10
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Date;
14import java.util.List;
15
16
17public class TimeSeriesRepositoryImpl implements TimeSeriesRepository {
18
19 private final MongoDatabase database;
20
21 public TimeSeriesRepositoryImpl() {
22 this.database = MongodbConfig.getDatabase();
23 }
24
25 @Override
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 @Override
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 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:
The formula it uses is the Pythagorean theorem. It goes a little like this:
  1. Find the difference between the current and previous latitude.
  2. Find the difference between the current and previous longitude.
  3. Square both differences and add them together.
  4. 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.
Services (the business logic)
Now that we have the repositories created and implemented, it's time to get down to business (logic).
Boat service
Let's start with the boat service. Create a service package, and we'll add our BoatService there.
1package com.mongodb.shiptracker.service;
2
3import com.mongodb.shiptracker.model.Boat;
4import com.mongodb.shiptracker.model.Location;
5import com.mongodb.shiptracker.model.Port;
6import com.mongodb.shiptracker.repository.BoatRepositoryImpl;
7import com.mongodb.shiptracker.repository.PortRepositoryImpl;
8
9public 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.
Port service
Next, we have our PortService.
1package com.mongodb.shiptracker.service;
2
3import com.mongodb.shiptracker.model.Port;
4import com.mongodb.shiptracker.repository.PortRepositoryImpl;
5
6import java.util.Arrays;
7import java.util.List;
8
9public 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).
Distance service
Now, we have a very simple DistanceService.
1package com.mongodb.shiptracker.service;
2
3import com.mongodb.shiptracker.repository.TimeSeriesRepositoryImpl;
4import org.bson.Document;
5
6import java.util.List;
7
8public 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}
Simulation
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.
1package com.mongodb.shiptracker.service;
2
3import com.mongodb.shiptracker.model.Boat;
4import com.mongodb.shiptracker.repository.TimeSeriesRepositoryImpl;
5
6import java.util.ArrayList;
7import java.util.List;
8
9public 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.
Controlling our API
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.
1package com.mongodb.shiptracker.controller;
2
3import com.mongodb.shiptracker.model.Boat;
4import com.mongodb.shiptracker.model.BoatRequest;
5import com.mongodb.shiptracker.service.BoatService;
6import com.mongodb.shiptracker.service.DistanceService;
7import com.mongodb.shiptracker.service.PortService;
8import com.mongodb.shiptracker.service.Simulator;
9import io.javalin.Javalin;
10import io.javalin.http.Context;
11
12public 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.
1package com.mongodb.shiptracker;
2
3import com.mongodb.shiptracker.controller.ShipTrackerController;
4import io.javalin.Javalin;
5
6public 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.
Running our app
Where the real fun begins. Let's compile and run our project with the following:
1mvn clean install
2mvn exec:java -Dexec.mainClass="com.mongodb.shiptracker.ShipTrackerApp"
Now, we can initialize our ports.
1curl -X POST http://localhost:7070/ports/init
With our ports added, let's make sure our boat’s endpoint is working.
1curl -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!
1curl -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.
1curl -X POST http://localhost:7070/simulate
In our terminal, we should see the boats moving along.
1Simulation step
2Boat BOAT001 moved to new position: (25.821515943344753, -79.86387750300888)
3Boat BOAT002 moved to new position: (40.70257614347035, -73.67282349442259)
And if we want to run our aggregation:
1curl -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!
Conclusion
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.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Quickstart

5 Different Ways to Deploy a Free Database with MongoDB Atlas


Feb 03, 2023 | 5 min read
Article

Building a Flask and MongoDB App with Azure Container Apps


Apr 02, 2024 | 8 min read
Tutorial

Adding Real-Time Notifications to Ghost CMS Using MongoDB and Server-Sent Events


Aug 14, 2023 | 7 min read
Tutorial

Utilizing PySpark to Connect MongoDB Atlas with Azure Databricks


Apr 02, 2024 | 6 min read
Table of Contents