diff --git a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java
index 460817d00fa210460fdb21ceb6ce33fb210dd674..8a2bbc21dde5f8baf441a4e7c89dfd2908b4aa26 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java
@@ -26,6 +26,7 @@ import org.eclipsefoundation.marketplace.dao.MongoDao;
 import org.eclipsefoundation.marketplace.exception.MaintenanceException;
 import org.eclipsefoundation.marketplace.model.MongoQuery;
 import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
+import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,16 +45,16 @@ import io.quarkus.mongodb.ReactiveMongoCollection;
 public class DefaultMongoDao implements MongoDao {
 	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMongoDao.class);
 
-	@ConfigProperty(name = "mongodb.database")
+	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_DB_NAME)
 	String databaseName;
 
-	@ConfigProperty(name = "mongodb.default.limit")
+	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_RETURN_LIMIT)
 	int defaultLimit;
 
-	@ConfigProperty(name = "mongodb.default.limit.max")
+	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_RETURN_LIMIT_MAX)
 	int defaultMax;
 
-	@ConfigProperty(name = "mongodb.maintenance", defaultValue = "false")
+	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_MAINTENANCE_FLAG, defaultValue = "false")
 	boolean maintenanceFlag;
 
 	@Inject
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java
index a6c93d3badc9da38f09e263ccfc937a236bd16f9..ec5396583fd0d0bb6c5e372be3fe29474db9800f 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java
@@ -63,6 +63,7 @@ public class Listing extends NodeBase {
 	private List<Author> authors;
 	private List<Tag> tags;
 	private List<ListingVersion> versions;
+	private boolean isPromotion;
 
 	/**
 	 * Default constructor, sets lists to empty lists to stop null pointers
@@ -381,6 +382,21 @@ public class Listing extends NodeBase {
 		this.versions = new ArrayList<>(versions);
 	}
 
+	/**
+	 * @return the isPromotion
+	 */
+	public boolean isPromotion() {
+		return isPromotion;
+	}
+
+	/**
+	 * @param isPromotion the isPromotion to set
+	 */
+	@JsonbTransient
+	public void setPromotion(boolean isPromotion) {
+		this.isPromotion = isPromotion;
+	}
+
 	@Override
 	public boolean validate() {
 		return super.validate() && license != null && !authors.isEmpty() && !categoryIds.isEmpty()
@@ -446,6 +462,7 @@ public class Listing extends NodeBase {
 		sb.append(", tags=").append(tags);
 		sb.append(", versions=").append(versions);
 		sb.append(", screenshots=").append(screenshots);
+		sb.append(", isPromotion=").append(isPromotion);
 		sb.append(']');
 		return sb.toString();
 	}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java
new file mode 100644
index 0000000000000000000000000000000000000000..3339a0d56d3ad841a42b2a8d764d5f639d2e688b
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java
@@ -0,0 +1,51 @@
+package org.eclipsefoundation.marketplace.dto;
+
+public class Promotion {
+
+	private String id;
+	private String listingId;
+	private int weight = 1;
+
+	/**
+	 * @return the id
+	 */
+	public String getId() {
+		return id;
+	}
+
+	/**
+	 * @param id the id to set
+	 */
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	/**
+	 * @return the listingId
+	 */
+	public String getListingId() {
+		return listingId;
+	}
+
+	/**
+	 * @param listingId the listingId to set
+	 */
+	public void setListingId(String listingId) {
+		this.listingId = listingId;
+	}
+
+	/**
+	 * @return the weight
+	 */
+	public int getWeight() {
+		return weight;
+	}
+
+	/**
+	 * @param weight the weight to set
+	 */
+	public void setWeight(int weight) {
+		this.weight = weight;
+	}
+
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java
new file mode 100644
index 0000000000000000000000000000000000000000..5af459f4f625423ba1f87aacf2c6f6d9c6508eb7
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java
@@ -0,0 +1,86 @@
+/* Copyright (c) 2019 Eclipse Foundation and others.
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Public License 2.0
+ * which is available at http://www.eclipse.org/legal/epl-v20.html,
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.marketplace.dto.codecs;
+
+import java.util.UUID;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bson.BsonReader;
+import org.bson.BsonString;
+import org.bson.BsonValue;
+import org.bson.BsonWriter;
+import org.bson.Document;
+import org.bson.codecs.Codec;
+import org.bson.codecs.CollectibleCodec;
+import org.bson.codecs.DecoderContext;
+import org.bson.codecs.EncoderContext;
+import org.eclipsefoundation.marketplace.dto.Promotion;
+import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+
+import com.mongodb.MongoClient;
+
+/**
+ * Codec for reading and writing {@linkplain Promotion} objectss to database objects.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class PromotionCodec implements CollectibleCodec<Promotion> {
+	
+	private final Codec<Document> documentCodec;
+
+	/**
+	 * Creates the codec and initializes the codecs and converters needed to create
+	 * a listing from end to end.
+	 */
+	public PromotionCodec() {
+		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
+	}
+
+	@Override
+	public void encode(BsonWriter writer, Promotion value, EncoderContext encoderContext) {
+		Document doc = new Document();
+		doc.put(DatabaseFieldNames.DOCID, value.getId());
+		doc.put(DatabaseFieldNames.LISTING_ID, value.getListingId());
+		documentCodec.encode(writer, doc, encoderContext);
+	}
+
+	@Override
+	public Class<Promotion> getEncoderClass() {
+		return Promotion.class;
+	}
+
+	@Override
+	public Promotion decode(BsonReader reader, DecoderContext decoderContext) {
+		Document value = documentCodec.decode(reader, decoderContext);
+
+		Promotion out = new Promotion();
+		out.setId(value.getString(DatabaseFieldNames.DOCID));
+		out.setListingId(value.getString(DatabaseFieldNames.LISTING_ID));
+		out.setWeight(value.getInteger(DatabaseFieldNames.PROMOTION_WEIGHTING, 1));
+		return out;
+	}
+
+	@Override
+	public Promotion generateIdIfAbsentFromDocument(Promotion document) {
+		if (!documentHasId(document)) {
+			document.setId(UUID.randomUUID().toString());
+		}
+		return document;
+	}
+
+	@Override
+	public boolean documentHasId(Promotion document) {
+		return !StringUtils.isBlank(document.getId());
+	}
+
+	@Override
+	public BsonValue getDocumentId(Promotion document) {
+		return new BsonString(document.getId());
+	}
+
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java
index e2aaae11e759b477c0182a58d85366be0b16323a..4c15814fee2cae9e232e0ed9fc20c61e20b6bdcd 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java
@@ -6,51 +6,21 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
 import javax.enterprise.context.ApplicationScoped;
 
-import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.Catalog;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-
-import com.mongodb.client.model.Filters;
 
 /**
- * Filter implementation for the Catalog class.
+ * Filter implementation for the {@link Catalog} class.
  * 
  * @author Martin Lowe
+ * 
  */
 @ApplicationScoped
 public class CatalogFilter implements DtoFilter<Catalog> {
 
-	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
-		List<Bson> filters = new ArrayList<>();
-		// perform following checks only if there is no doc root
-		if (root == null) {
-			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
-			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
-			}
-		}
-		return filters;
-	}
-	
-	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
-		return Collections.emptyList();
-	}
-
 	@Override
 	public Class<Catalog> getType() {
 		return Catalog.class;
 	}
-
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java
index 3092ecf53f48b7dbeaf00bbf31eb386a8e859034..7f7bfd3614b92aef175f0bf14c4eaf85cb72d563 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java
@@ -6,47 +6,20 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
 import javax.enterprise.context.ApplicationScoped;
 
-import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.Category;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
-import com.mongodb.client.model.Filters;
 
 /**
- * @author martin
- *
+ * Filter implementation for the {@link Category} class.
+ * 
+ * @author Martin Lowe
+ * 
  */
 @ApplicationScoped
 public class CategoryFilter implements DtoFilter<Category> {
 
-	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
-		List<Bson> filters = new ArrayList<>();
-		// perform following checks only if there is no doc root
-		if (root == null) {
-			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
-			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
-			}
-		}
-		return filters;
-	}
-
-	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
-		return Collections.emptyList();
-	}
-
 	@Override
 	public Class<Category> getType() {
 		return Category.class;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java
index bb5d8e05a9cfa228241336aca7604afb710847f9..d4cedace6e3e6d1fe0282604708d03ed3ce203e7 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java
@@ -6,10 +6,15 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 import org.bson.conversions.Bson;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
+import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
 import com.mongodb.client.model.Aggregates;
 import com.mongodb.client.model.Filters;
@@ -24,21 +29,35 @@ public interface DtoFilter<T> {
 	/**
 	 * Retrieve filter objects for the current arguments.
 	 * 
-	 * @param wrap       wrapper for the current request
+	 * @param params     parameters to use in filter construction
 	 * @param nestedPath current path for nesting of filters
 	 * @return list of filters for the current request, or empty if there are no
 	 *         applicable filters.
 	 */
-	List<Bson> getFilters(RequestWrapper wrap, String nestedPath);
+	default List<Bson> getFilters(QueryParameters params, String root) {
+		List<Bson> filters = new ArrayList<>();
+		// perform following checks only if there is no doc root
+		if (root == null) {
+			// ID check
+			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
+			if (id.isPresent()) {
+				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
+			}
+		}
+		return filters;
+
+	}
 
 	/**
 	 * Retrieve aggregate filter operations for the current arguments.
 	 * 
-	 * @param wrap wrapper for the current request
+	 * @param params parameters to use in aggregate construction
 	 * @return list of aggregates for the current request, or empty if there are no
 	 *         applicable aggregates.
 	 */
-	List<Bson> getAggregates(RequestWrapper wrap);
+	default List<Bson> getAggregates(QueryParameters params) {
+		return Collections.emptyList();
+	}
 
 	/**
 	 * Returns the type of data this object will filter for.
@@ -52,19 +71,19 @@ public interface DtoFilter<T> {
 	 * match operation to port filter operations into an aggregate pipeline. This is
 	 * handy when importing nested types and enabling filters.
 	 * 
-	 * @param wrap       wrapper for the current request
+	 * @param params     parameters for the current call
 	 * @param nestedPath current path for nesting of filters
 	 * @return a list of aggregate pipeline operations representing the filters for
 	 *         the current request.
 	 */
-	default Bson wrapFiltersToAggregate(RequestWrapper wrap, String nestedPath) {
-		List<Bson> filters = getFilters(wrap, nestedPath);
+	default Bson wrapFiltersToAggregate(QueryParameters params, String nestedPath) {
+		List<Bson> filters = getFilters(params, nestedPath);
 		if (!filters.isEmpty()) {
 			return Aggregates.match(Filters.elemMatch(nestedPath, Filters.and(filters)));
 		}
 		return null;
 	}
-	
+
 	/**
 	 * Whether this type of data should be restrained to a limited set, or return
 	 * all data that is found.
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java
index 3f1ff92874463f8a42cb744b58941091db114cad..6f5f4618ef2012d09b87d82740dd21cdacbafb83 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java
@@ -15,7 +15,7 @@ import javax.enterprise.context.ApplicationScoped;
 
 import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.ErrorReport;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
@@ -30,40 +30,40 @@ import com.mongodb.client.model.Filters;
 public class ErrorReportFilter implements DtoFilter<ErrorReport> {
 
 	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
+	public List<Bson> getFilters(QueryParameters params, String root) {
 		List<Bson> filters = new ArrayList<>();
 
 		// ErrorReport ID check
-		Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+		Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
 		if (id.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
 		}
 
 		// select by multiple IDs
-		List<String> ids = wrap.getParams(UrlParameterNames.IDS);
+		List<String> ids = params.getValues(UrlParameterNames.IDS.getParameterName());
 		if (!ids.isEmpty()) {
 			filters.add(Filters.in(DatabaseFieldNames.DOCID, ids));
 		}
 
 		// listing ID check
-		Optional<String> listingId = wrap.getFirstParam(UrlParameterNames.LISTING_ID);
+		Optional<String> listingId = params.getFirstIfPresent(UrlParameterNames.LISTING_ID.getParameterName());
 		if (listingId.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.LISTING_ID, listingId.get()));
 		}
 
 		// listing ID check
-		Optional<String> isRead = wrap.getFirstParam(UrlParameterNames.READ);
+		Optional<String> isRead = params.getFirstIfPresent(UrlParameterNames.READ.getParameterName());
 		if (isRead.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.ERROR_READ, Boolean.valueOf(isRead.get())));
 		}
 		
 		// select by feature ID
-		List<String> featureId = wrap.getParams(UrlParameterNames.FEATURE_ID);
+		List<String> featureId = params.getValues(UrlParameterNames.FEATURE_ID.getParameterName());
 		if (!featureId.isEmpty()) {
 			filters.add(Filters.in(DatabaseFieldNames.ERROR_FEATURE_IDS, featureId));
 		}
 		// text search
-		Optional<String> text = wrap.getFirstParam(UrlParameterNames.QUERY_STRING);
+		Optional<String> text = params.getFirstIfPresent(UrlParameterNames.QUERY_STRING.getParameterName());
 		if (text.isPresent()) {
 			filters.add(Filters.text(text.get()));
 		}
@@ -71,7 +71,7 @@ public class ErrorReportFilter implements DtoFilter<ErrorReport> {
 	}
 
 	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
+	public List<Bson> getAggregates(QueryParameters params) {
 		return Collections.emptyList();
 	}
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java
index e7c1e9954e439120f3f9405f3a688d45e9118160..2cd260db2b78ee2b7f85bccb56130b32221f09da 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java
@@ -17,7 +17,7 @@ import javax.enterprise.context.ApplicationScoped;
 import org.apache.commons.lang3.StringUtils;
 import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.Install;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
@@ -32,39 +32,39 @@ import com.mongodb.client.model.Filters;
 public class InstallFilter implements DtoFilter<Install> {
 
 	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
+	public List<Bson> getFilters(QueryParameters params, String root) {
 		List<Bson> filters = new ArrayList<>();
 		// perform following checks only if there is no doc root
 		if (root == null) {
 			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
 			if (id.isPresent()) {
 				filters.add(Filters.eq(DatabaseFieldNames.LISTING_ID, id.get()));
 			}
 		}
 		// version check
-		Optional<String> version = wrap.getFirstParam(UrlParameterNames.VERSION);
+		Optional<String> version = params.getFirstIfPresent(UrlParameterNames.VERSION.getParameterName());
 		if (version.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.INSTALL_VERSION, version.get()));
 		}
 		// OS filter
-		Optional<String> os = wrap.getFirstParam(UrlParameterNames.OS);
+		Optional<String> os = params.getFirstIfPresent(UrlParameterNames.OS.getParameterName());
 		if (os.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.OS, os.get()));
 		}
 		// eclipse version
-		Optional<String> eclipseVersion = wrap.getFirstParam(UrlParameterNames.ECLIPSE_VERSION);
+		Optional<String> eclipseVersion = params.getFirstIfPresent(UrlParameterNames.ECLIPSE_VERSION.getParameterName());
 		if (eclipseVersion.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.ECLIPSE_VERSION, eclipseVersion.get()));
 		}
 		// TODO this sorts by naturally by character rather than by actual number (e.g.
 		// 1.9 is technically greater than 1.10)
 		// solution version - Java version
-		Optional<String> javaVersion = wrap.getFirstParam(UrlParameterNames.JAVA_VERSION);
+		Optional<String> javaVersion = params.getFirstIfPresent(UrlParameterNames.JAVA_VERSION.getParameterName());
 		if (javaVersion.isPresent()) {
 			filters.add(Filters.gte(DatabaseFieldNames.INSTALL_JAVA_VERSION, javaVersion.get()));
 		}
-		Optional<String> date = wrap.getFirstParam(UrlParameterNames.DATE_FROM);
+		Optional<String> date = params.getFirstIfPresent(UrlParameterNames.DATE_FROM.getParameterName());
 		if (date.isPresent() && StringUtils.isNumeric(date.get())) {
 			filters.add(Filters.gte(DatabaseFieldNames.INSTALL_DATE, new Date(Integer.valueOf(date.get()))));
 		}
@@ -72,7 +72,7 @@ public class InstallFilter implements DtoFilter<Install> {
 	}
 
 	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
+	public List<Bson> getAggregates(QueryParameters params) {
 		return Collections.emptyList();
 	}
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java
index 38e75c081cbd92a0b6feed2be1c50b6bff033e56..3d8474901b063d9625417fc1982b9017739ad1ca 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java
@@ -6,20 +6,9 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
 import javax.enterprise.context.ApplicationScoped;
 
-import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.InstallMetrics;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-
-import com.mongodb.client.model.Filters;
 
 /**
  * Filter implementation for the {@linkplain InstallMetrics} class.
@@ -30,25 +19,6 @@ import com.mongodb.client.model.Filters;
 @ApplicationScoped
 public class InstallMetricsFilter implements DtoFilter<InstallMetrics> {
 
-	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
-		List<Bson> filters = new ArrayList<>();
-		// perform following checks only if there is no doc root
-		if (root == null) {
-			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
-			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
-			}
-		}
-		return filters;
-	}
-
-	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
-		return Collections.emptyList();
-	}
-
 	@Override
 	public Class<InstallMetrics> getType() {
 		return InstallMetrics.class;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java
index 9aadaa4e0d7ff710be10a06ef3468bf0c22b7ac1..2687c7933e1b5fc7266820c80a323c658b6be1f6 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java
@@ -16,7 +16,7 @@ import javax.inject.Inject;
 import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.Listing;
 import org.eclipsefoundation.marketplace.dto.ListingVersion;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
@@ -39,37 +39,37 @@ public class ListingFilter implements DtoFilter<Listing> {
 	DtoFilter<ListingVersion> listingVersionFilter;
 	
 	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
+	public List<Bson> getFilters(QueryParameters params, String root) {
 		List<Bson> filters = new ArrayList<>();
 		// perform following checks only if there is no doc root
 		if (root == null) {
 			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
 			if (id.isPresent()) {
 				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
 			}
 		}
 
 		// select by multiple IDs
-		List<String> ids = wrap.getParams(UrlParameterNames.IDS);
+		List<String> ids = params.getValues(UrlParameterNames.IDS.getParameterName());
 		if (!ids.isEmpty()) {
 			filters.add(Filters.in(DatabaseFieldNames.DOCID, ids));
 		}
 
 		// Listing license type check
-		Optional<String> licType = wrap.getFirstParam(DatabaseFieldNames.LICENSE_TYPE);
+		Optional<String> licType = params.getFirstIfPresent(DatabaseFieldNames.LICENSE_TYPE);
 		if (licType.isPresent()) {
 			filters.add(Filters.eq(DatabaseFieldNames.LICENSE_TYPE, licType.get()));
 		}
 
 		// select by multiple tags
-		List<String> tags = wrap.getParams(UrlParameterNames.TAGS);
+		List<String> tags = params.getValues(UrlParameterNames.TAGS.getParameterName());
 		if (!tags.isEmpty()) {
 			filters.add(Filters.in(DatabaseFieldNames.LISTING_TAGS + ".title", tags));
 		}
 
 		// text search
-		Optional<String> text = wrap.getFirstParam(UrlParameterNames.QUERY_STRING);
+		Optional<String> text = params.getFirstIfPresent(UrlParameterNames.QUERY_STRING.getParameterName());
 		if (text.isPresent()) {
 			filters.add(Filters.text(text.get()));
 		}
@@ -77,18 +77,18 @@ public class ListingFilter implements DtoFilter<Listing> {
 	}
 
 	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
+	public List<Bson> getAggregates(QueryParameters params) {
 		List<Bson> aggs = new ArrayList<>();
 		// adds a $lookup aggregate, joining categories on categoryIDS as "categories"
 		aggs.add(Aggregates.lookup(DtoTableNames.LISTING_VERSION.getTableName(), DatabaseFieldNames.DOCID, DatabaseFieldNames.LISTING_ID,
 				DatabaseFieldNames.LISTING_VERSIONS));
-		Bson filters = listingVersionFilter.wrapFiltersToAggregate(wrap, DatabaseFieldNames.LISTING_VERSIONS);
+		Bson filters = listingVersionFilter.wrapFiltersToAggregate(params, DatabaseFieldNames.LISTING_VERSIONS);
 		if (filters != null) {
 			aggs.add(filters);
 		}
 		aggs.add(Aggregates.lookup(DtoTableNames.CATEGORY.getTableName(), DatabaseFieldNames.CATEGORY_IDS,
 				DatabaseFieldNames.DOCID, DatabaseFieldNames.LISTING_CATEGORIES));
-		List<String> marketIds = wrap.getParams(UrlParameterNames.MARKET_IDS);
+		List<String> marketIds = params.getValues(UrlParameterNames.MARKET_IDS.getParameterName());
 		if (!marketIds.isEmpty()) {
 			aggs.add(Aggregates.match(Filters.in("categories.market_ids", marketIds)));
 		}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java
index ddc414b1ea60595807964bb56ee204594740389e..4334ca984077adafcdbcd33e372e5a3b6d417a01 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java
@@ -16,7 +16,7 @@ import javax.enterprise.context.ApplicationScoped;
 import org.apache.commons.lang3.StringUtils;
 import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.ListingVersion;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
@@ -32,29 +32,29 @@ import com.mongodb.client.model.Filters;
 public class ListingVersionFilter implements DtoFilter<ListingVersion> {
 
 	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
+	public List<Bson> getFilters(QueryParameters params, String root) {
 		List<Bson> filters = new ArrayList<>();
 		// perform following checks only if there is no doc root
 		if (root == null) {
 			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
 			if (id.isPresent()) {
 				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
 			}
 		}
 
 		// solution version - OS filter
-		Optional<String> os = wrap.getFirstParam(UrlParameterNames.OS);
+		Optional<String> os = params.getFirstIfPresent(UrlParameterNames.OS.getParameterName());
 		if (os.isPresent()) {
 			filters.add(Filters.eq("platforms", os.get()));
 		}
 		// solution version - eclipse version
-		Optional<String> eclipseVersion = wrap.getFirstParam(UrlParameterNames.ECLIPSE_VERSION);
+		Optional<String> eclipseVersion = params.getFirstIfPresent(UrlParameterNames.ECLIPSE_VERSION.getParameterName());
 		if (eclipseVersion.isPresent()) {
 			filters.add(Filters.eq("compatible_versions", eclipseVersion.get()));
 		}
 		// solution version - Java version
-		Optional<String> javaVersion = wrap.getFirstParam(UrlParameterNames.JAVA_VERSION);
+		Optional<String> javaVersion = params.getFirstIfPresent(UrlParameterNames.JAVA_VERSION.getParameterName());
 		if (javaVersion.isPresent() && StringUtils.isNumeric(javaVersion.get())) {
 			filters.add(Filters.gte("min_java_version", Integer.valueOf(javaVersion.get())));
 		}
@@ -63,7 +63,7 @@ public class ListingVersionFilter implements DtoFilter<ListingVersion> {
 	}
 
 	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
+	public List<Bson> getAggregates(QueryParameters params) {
 		return Collections.emptyList();
 	}
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java
index 664333f0dd70f7c7fac09f7fad41507b1ed15461..f0dc7b5bff5595fe0b93fbea98750f16f43f496c 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java
@@ -20,7 +20,7 @@ import javax.enterprise.context.ApplicationScoped;
 import org.bson.BsonArray;
 import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.Market;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
@@ -40,12 +40,12 @@ import com.mongodb.client.model.Variable;
 public class MarketFilter implements DtoFilter<Market> {
 
 	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
+	public List<Bson> getFilters(QueryParameters params, String root) {
 		List<Bson> filters = new ArrayList<>();
 		// perform following checks only if there is no doc root
 		if (root == null) {
 			// ID check
-			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
 			if (id.isPresent()) {
 				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
 			}
@@ -54,7 +54,7 @@ public class MarketFilter implements DtoFilter<Market> {
 	}
 
 	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
+	public List<Bson> getAggregates(QueryParameters params) {
 		List<Bson> aggs = new ArrayList<>();
 
 		String tempFieldName = "tmp";
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java
index 1f6b9bbc8e7f1f3911eb7d58318a5eecf3b1547a..59bf384535084921c92503597b5229da6c97dbe3 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java
@@ -19,7 +19,7 @@ import org.bson.BsonDocument;
 import org.bson.BsonString;
 import org.bson.conversions.Bson;
 import org.eclipsefoundation.marketplace.dto.MetricPeriod;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
@@ -38,15 +38,15 @@ import com.mongodb.client.model.Projections;
 public class MetricPeriodFilter implements DtoFilter<MetricPeriod> {
 
 	@Override
-	public List<Bson> getFilters(RequestWrapper wrap, String root) {
+	public List<Bson> getFilters(QueryParameters params, String root) {
 		return Collections.emptyList();
 	}
 
 	@Override
-	public List<Bson> getAggregates(RequestWrapper wrap) {
+	public List<Bson> getAggregates(QueryParameters params) {
 		// check that we have required fields first
-		Optional<String> startDate = wrap.getFirstParam(UrlParameterNames.START);
-		Optional<String> endDate = wrap.getFirstParam(UrlParameterNames.END);
+		Optional<String> startDate = params.getFirstIfPresent(UrlParameterNames.START.getParameterName());
+		Optional<String> endDate = params.getFirstIfPresent(UrlParameterNames.END.getParameterName());
 		List<Bson> aggregates = new ArrayList<>();
 		if (startDate.isPresent() && endDate.isPresent()) {
 			// check for all listings that are after the start date
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7852402e29dca1aca9cb4663d834897e3783066
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java
@@ -0,0 +1,27 @@
+/* Copyright (c) 2019 Eclipse Foundation and others.
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Public License 2.0
+ * which is available at http://www.eclipse.org/legal/epl-v20.html,
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.marketplace.dto.filter;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipsefoundation.marketplace.dto.Promotion;
+
+
+/**
+ * Filter implementation for the {@link Promotion} class.
+ * 
+ * @author Martin Lowe
+ * 
+ */
+@ApplicationScoped
+public class PromotionFilter implements DtoFilter<Promotion> {
+
+	@Override
+	public Class<Promotion> getType() {
+		return Promotion.class;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..80f26042991aab079f009dbe17db60b14bd27c95
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java
@@ -0,0 +1,35 @@
+/* Copyright (c) 2019 Eclipse Foundation and others.
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Public License 2.0
+ * which is available at http://www.eclipse.org/legal/epl-v20.html,
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.marketplace.dto.providers;
+
+import org.bson.codecs.Codec;
+import org.bson.codecs.configuration.CodecProvider;
+import org.bson.codecs.configuration.CodecRegistry;
+import org.eclipsefoundation.marketplace.dto.Promotion;
+import org.eclipsefoundation.marketplace.dto.codecs.PromotionCodec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides the {@link PromotionCodec} to MongoDB for conversions of
+ * {@link Promotion} objects.
+ * 
+ * @author Martin Lowe
+ */
+public class PromotionCodecProvider implements CodecProvider {
+	private static final Logger LOGGER = LoggerFactory.getLogger(PromotionCodecProvider.class);
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
+		if (clazz == Promotion.class) {
+			LOGGER.debug("Registering custom Promotion class MongoDB codec");
+			return (Codec<T>) new PromotionCodec();
+		}
+		return null;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java
index 547f3ba149d5bc60da6cddcd4925d913bb509449..033f3907626238a07e3b504da8b109ce7bc2e0c7 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java
@@ -7,7 +7,9 @@
 package org.eclipsefoundation.marketplace.model;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 import org.apache.commons.lang3.StringUtils;
@@ -26,15 +28,14 @@ import com.mongodb.client.model.Filters;
 
 /**
  * Wrapper for initializing MongoDB BSON filters, sort clauses, and document
- * type when interacting with MongoDB. This should only be called from within
- * the scope of a request with a defined {@link ResourceDataType}
+ * type when interacting with MongoDB.
  * 
  * @author Martin Lowe
  */
 public class MongoQuery<T> {
 	private static final Logger LOGGER = LoggerFactory.getLogger(MongoQuery.class);
 
-	private RequestWrapper wrapper;
+	private QueryParameters params;
 	private DtoFilter<T> dtoFilter;
 
 	private Bson filter;
@@ -43,9 +44,14 @@ public class MongoQuery<T> {
 	private List<Bson> aggregates;
 
 	public MongoQuery(RequestWrapper wrapper, DtoFilter<T> dtoFilter) {
-		this.wrapper = wrapper;
+		this(wrapper, Collections.emptyMap(), dtoFilter);
+	}
+
+	public MongoQuery(RequestWrapper wrapper, Map<String, List<String>> params, DtoFilter<T> dtoFilter) {
 		this.dtoFilter = dtoFilter;
 		this.aggregates = new ArrayList<>();
+		// allow for parameters to be either explicitly set or use wrapper params
+		this.params = new QueryParameters(wrapper == null ? params : wrapper.asMap());
 		init();
 	}
 
@@ -54,7 +60,7 @@ public class MongoQuery<T> {
 	 * type object. This can be called again to reset the parameters if needed due
 	 * to updated fields.
 	 */
-	public void init() {
+	private void init() {
 		// clear old values if set to default
 		this.filter = null;
 		this.sort = null;
@@ -63,10 +69,10 @@ public class MongoQuery<T> {
 
 		// get the filters for the current DTO
 		List<Bson> filters = new ArrayList<>();
-		filters.addAll(dtoFilter.getFilters(wrapper, null));
+		filters.addAll(dtoFilter.getFilters(params, null));
 
 		// get fields that make up the required fields to enable pagination and check
-		Optional<String> sortOpt = wrapper.getFirstParam(UrlParameterNames.SORT);
+		Optional<String> sortOpt = params.getFirstIfPresent(UrlParameterNames.SORT.getParameterName());
 		if (sortOpt.isPresent()) {
 			String sortVal = sortOpt.get();
 			SortOrder ord = SortOrder.getOrderFromValue(sortOpt.get());
@@ -76,13 +82,14 @@ public class MongoQuery<T> {
 			if (SortOrder.RANDOM.equals(ord)) {
 				this.order = SortOrder.RANDOM;
 			} else if (ord != SortOrder.NONE) {
-				setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1), filters);
+				setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1));
 			}
 		}
+
 		if (!filters.isEmpty()) {
 			this.filter = Filters.and(filters);
 		}
-		this.aggregates = dtoFilter.getAggregates(wrapper);
+		this.aggregates = dtoFilter.getAggregates(params);
 
 		if (LOGGER.isDebugEnabled()) {
 			LOGGER.debug("MongoDB query initialized with filter: {}", this.filter);
@@ -106,20 +113,21 @@ public class MongoQuery<T> {
 			out.add(Aggregates.match(filter));
 		}
 		// add base aggregates (joins)
-		out.addAll(aggregates); 
+		out.addAll(aggregates);
 		if (sort != null) {
 			out.add(Aggregates.sort(sort));
 		}
 		// check if the page param has been set, defaulting to the first page if not set
-		Optional<String> pageOpt = wrapper.getFirstParam(UrlParameterNames.PAGE);
 		int page = 1;
+		Optional<String> pageOpt = params.getFirstIfPresent(UrlParameterNames.PAGE.getParameterName());
 		if (pageOpt.isPresent() && StringUtils.isNumeric(pageOpt.get())) {
 			int tmpPage = Integer.parseInt(pageOpt.get());
 			if (tmpPage > 0) {
 				page = tmpPage;
-				LOGGER.debug("Found a set page of {} for current query",page);
+				LOGGER.debug("Found a set page of {} for current query", page);
 			}
 		}
+		
 		out.add(Aggregates.skip((page - 1) * limit));
 		// add sample if we aren't sorting
 		if ((sort == null || SortOrder.RANDOM.equals(order)) && dtoFilter.useLimit()) {
@@ -136,14 +144,14 @@ public class MongoQuery<T> {
 	 *         present and numeric, otherwise returns -1.
 	 */
 	public int getLimit() {
-		Optional<String> limitVal = wrapper.getFirstParam(UrlParameterNames.LIMIT);
+		Optional<String> limitVal = params.getFirstIfPresent(UrlParameterNames.LIMIT.getParameterName());
 		if (limitVal.isPresent() && StringUtils.isNumeric(limitVal.get())) {
 			return Integer.parseInt(limitVal.get());
 		}
 		return -1;
 	}
 
-	private void setSort(String sortField, String sortOrder, List<Bson> filters) {
+	private void setSort(String sortField, String sortOrder) {
 		List<Sortable<?>> fields = SortableHelper.getSortableFields(getDocType());
 		Optional<Sortable<?>> fieldContainer = SortableHelper.getSortableFieldByName(fields, sortField);
 		if (fieldContainer.isPresent()) {
@@ -186,20 +194,6 @@ public class MongoQuery<T> {
 		return dtoFilter.getType();
 	}
 
-	/**
-	 * @return the wrapper
-	 */
-	public RequestWrapper getWrapper() {
-		return wrapper;
-	}
-
-	/**
-	 * @param wrapper the wrapper to set
-	 */
-	public void setWrapper(RequestWrapper wrapper) {
-		this.wrapper = wrapper;
-	}
-
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder();
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java b/src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java
new file mode 100644
index 0000000000000000000000000000000000000000..d91173a6ad63cac4e777455eae587bf5c76c9699
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java
@@ -0,0 +1,73 @@
+package org.eclipsefoundation.marketplace.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Container for query parameters, using a map allowing for multiple values set
+ * to a single key.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class QueryParameters {
+	private final Map<String, List<String>> parameters;
+
+	/**
+	 * Generates an empty internal parameter map
+	 */
+	public QueryParameters() {
+		this(Collections.emptyMap());
+	}
+
+	/**
+	 * Creates a copy of the passed parameter map.
+	 * 
+	 * @param parameters map of parameter keys and values to use.
+	 */
+	public QueryParameters(Map<String, List<String>> parameters) {
+		this.parameters = new HashMap<>(parameters);
+	}
+
+	/**
+	 * Returns a copy of the values available for the given key.
+	 * 
+	 * @param key string key to retrieve values for
+	 * @return list of values if set, or an empty list.
+	 */
+	public List<String> getValues(String key) {
+		return parameters.getOrDefault(key, Collections.emptyList());
+	}
+
+	/**
+	 * Helper for map to retrieve first value available if one has been set for
+	 * params.
+	 * 
+	 * @param key    key to retrieve first value for
+	 * @param params parameter map for query to retrieve value from
+	 * @return value wrapped in optional if present, otherwise empty optional.
+	 */
+	public Optional<String> getFirstIfPresent(String key) {
+		List<String> vals = parameters.get(key);
+		if (vals != null && !vals.isEmpty()) {
+			return Optional.ofNullable(vals.get(0));
+		}
+		return Optional.empty();
+	}
+
+	public void add(String key, String value) {
+		this.parameters.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
+	}
+
+	public void remove(String key) {
+		this.parameters.remove(key);
+	}
+
+	public Map<String, List<String>> asMap() {
+		return new HashMap<>(parameters);
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java
index 43fc3469c2b916fc78933073cc350bddcf709a25..0fd4ce782a86eeda26aa84398d242cd9f4749f92 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java
@@ -6,7 +6,6 @@
  */
 package org.eclipsefoundation.marketplace.model;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -14,6 +13,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 import javax.enterprise.context.RequestScoped;
 import javax.servlet.http.HttpServletRequest;
@@ -23,6 +23,7 @@ import javax.ws.rs.core.UriInfo;
 import org.apache.commons.lang3.StringUtils;
 import org.eclipsefoundation.marketplace.namespace.DeprecatedHeader;
 import org.eclipsefoundation.marketplace.namespace.RequestHeaderNames;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 import org.eclipsefoundation.marketplace.request.CacheBypassFilter;
 import org.jboss.resteasy.core.ResteasyContext;
 
@@ -38,7 +39,7 @@ import org.jboss.resteasy.core.ResteasyContext;
 public class RequestWrapper {
 	private static final String EMPTY_KEY_MESSAGE = "Key must not be null or blank";
 
-	private Map<String, List<String>> params;
+	private QueryParameters params;
 
 	private UriInfo uriInfo;
 	private HttpServletRequest request;
@@ -65,12 +66,12 @@ public class RequestWrapper {
 	 * @return the first value set in the parameter map for the given key, or null
 	 *         if absent.
 	 */
-	public Optional<String> getFirstParam(String key) {
-		if (StringUtils.isBlank(key)) {
+	public Optional<String> getFirstParam(UrlParameterNames parameter) {
+		if (parameter == null) {
 			throw new IllegalArgumentException(EMPTY_KEY_MESSAGE);
 		}
 
-		List<String> vals = getParams().get(key);
+		List<String> vals = getParams().getValues(parameter.getParameterName());
 		if (vals == null || vals.isEmpty()) {
 			return Optional.empty();
 		}
@@ -85,12 +86,12 @@ public class RequestWrapper {
 	 * @return the value list for the given key if it exists, or an empty collection
 	 *         if none exists.
 	 */
-	public List<String> getParams(String key) {
-		if (StringUtils.isBlank(key)) {
+	public List<String> getParams(UrlParameterNames parameter) {
+		if (parameter == null) {
 			throw new IllegalArgumentException(EMPTY_KEY_MESSAGE);
 		}
 
-		List<String> vals = getParams().get(key);
+		List<String> vals = getParams().getValues(parameter.getParameterName());
 		if (vals == null || vals.isEmpty()) {
 			return Collections.emptyList();
 		}
@@ -101,23 +102,23 @@ public class RequestWrapper {
 	 * Adds the given value for the given key, preserving previous values if they
 	 * exist.
 	 * 
-	 * @param key     string key to add the value to, must not be null
-	 * @param value   the value to add to the key
+	 * @param key   string key to add the value to, must not be null
+	 * @param value the value to add to the key
 	 */
 	public void addParam(String key, String value) {
 		if (StringUtils.isBlank(key)) {
 			throw new IllegalArgumentException(EMPTY_KEY_MESSAGE);
 		}
 		Objects.requireNonNull(value);
-		getParams().computeIfAbsent(key, k -> new ArrayList<>()).add(value);
+		getParams().add(key, value);
 	}
 
 	/**
 	 * Sets the value as the value for the given key, removing previous values if
 	 * they exist.
 	 * 
-	 * @param key     string key to add the value to, must not be null
-	 * @param value   the value to add to the key
+	 * @param key   string key to add the value to, must not be null
+	 * @param value the value to add to the key
 	 */
 	public void setParam(String key, String value) {
 		if (StringUtils.isBlank(key)) {
@@ -129,6 +130,11 @@ public class RequestWrapper {
 		addParam(key, value);
 	}
 
+	public List<UrlParameterNames> getActiveParameters() {
+		return params.asMap().keySet().stream().map(UrlParameterNames::getByParameterName).filter(Objects::nonNull)
+				.collect(Collectors.toList());
+	}
+
 	/**
 	 * Returns this QueryParams object as a Map of param values indexed by the param
 	 * name.
@@ -136,15 +142,16 @@ public class RequestWrapper {
 	 * @return a copy of the internal param map
 	 */
 	public Map<String, List<String>> asMap() {
-		return new HashMap<>(getParams());
+		return new HashMap<>(getParams().asMap());
 	}
 
-	private Map<String, List<String>> getParams() {
+	private QueryParameters getParams() {
 		if (params == null) {
-			params = new HashMap<>();
+			Map<String, List<String>> requestParams = new HashMap<>();
 			if (uriInfo != null) {
-				params.putAll(uriInfo.getQueryParameters());
+				requestParams.putAll(uriInfo.getQueryParameters());
 			}
+			params = new QueryParameters(requestParams);
 		}
 		return this.params;
 	}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java
index f3d5f575d09a909a509a13979c8389951f126c53..b6cb1af181a5c5cd5b453a81ae4fe47d7984304f 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java
@@ -88,6 +88,8 @@ public final class DatabaseFieldNames {
 	public static final String MONTH_OFFSET_PREFIX = "offset_";
 	public static final String LISTING_IDS = "listing_ids";
 	
+	public static final String PROMOTION_WEIGHTING = "weight";
+	
 	private DatabaseFieldNames() {
 	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java
index a3d92ef242999a641a750c1f35fd8526f72119d7..c81d4b2eea8206ec03e44993220f93f33a4305c4 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java
@@ -15,6 +15,7 @@ import org.eclipsefoundation.marketplace.dto.Listing;
 import org.eclipsefoundation.marketplace.dto.ListingVersion;
 import org.eclipsefoundation.marketplace.dto.Market;
 import org.eclipsefoundation.marketplace.dto.MetricPeriod;
+import org.eclipsefoundation.marketplace.dto.Promotion;
 
 /**
  * Mapping of DTO classes to their respective tables in the DB.
@@ -26,7 +27,7 @@ public enum DtoTableNames {
 	LISTING(Listing.class, "listings"), CATEGORY(Category.class, "categories"), CATALOG(Catalog.class, "catalogs"),
 	MARKET(Market.class, "markets"), ERRORREPORT(ErrorReport.class, "errorreports"), INSTALL(Install.class, "installs"),
 	INSTALL_METRIC(InstallMetrics.class, "install_metrics"), METRIC_PERIOD(MetricPeriod.class, "installs"),
-	LISTING_VERSION(ListingVersion.class, "listing_versions");
+	LISTING_VERSION(ListingVersion.class, "listing_versions"),PROMOTION(Promotion.class, "promotion");
 
 	private Class<?> baseClass;
 	private String tableName;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ae381e871779cb7439e35e6d012065b50123480
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java
@@ -0,0 +1,21 @@
+package org.eclipsefoundation.marketplace.namespace;
+
+/**
+ * Contains Microprofile property names used by this application.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class MicroprofilePropertyNames {
+	public static final String PROMO_WEIGHT_DEFAULT = "eclipse.promotion.weighting.default";
+	public static final String PROMO_SERVE_COUNT = "eclipse.promotion.serve-count";
+	public static final String CACHE_TTL_MAX_SECONDS = "cache.ttl.write.seconds";
+	public static final String CACHE_SIZE_MAX = "cache.max.size";
+	public static final String MONGODB_DB_NAME = "mongodb.database";
+	public static final String MONGODB_RETURN_LIMIT = "mongodb.default.limit";
+	public static final String MONGODB_RETURN_LIMIT_MAX = "mongodb.default.limit.max";
+	public static final String MONGODB_MAINTENANCE_FLAG = "mongodb.maintenance";
+	
+	private MicroprofilePropertyNames() {
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java
index 94045917d1f52f458542dee941d48ba7b40f8bc6..cfd91e6a29da4ff30560af7bcdb727a2576dc697 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java
@@ -14,27 +14,51 @@ package org.eclipsefoundation.marketplace.namespace;
  * 
  * @author Martin Lowe
  */
-public final class UrlParameterNames {
+public enum UrlParameterNames {
 
-	public static final String QUERY_STRING = "q";
-	public static final String PAGE = "page";
-	public static final String LIMIT = "limit";
-	public static final String SORT = "sort";
-	public static final String OS = "os";
-	public static final String ECLIPSE_VERSION = "eclipse_version";
-	public static final String JAVA_VERSION = "min_java_version";
-	public static final String IDS = "ids";
-	public static final String TAGS = "tags";
-	public static final String MARKET_IDS = "market_ids";
-	public static final String ID = "id";
-	public static final String LISTING_ID = "listing_id";
-	public static final String READ = "read";
-	public static final String FEATURE_ID = "feature_id";
-	public static final String VERSION = "version";
-	public static final String DATE_FROM = "from";
-	public static final String END = "end";
-	public static final String START = "start";
+	QUERY_STRING("q"),
+	PAGE("page"),
+	LIMIT("limit"),
+	SORT("sort"),
+	OS("os"),
+	ECLIPSE_VERSION("eclipse_version"),
+	JAVA_VERSION("min_java_version"),
+	IDS("ids"),
+	TAGS("tags"),
+	MARKET_IDS("market_ids"),
+	ID("id"),
+	LISTING_ID("listing_id"),
+	READ("read"),
+	FEATURE_ID("feature_id"),
+	VERSION("version"),
+	DATE_FROM("from"),
+	END("end"),
+	START("start");
+
+	private String parameterName;
+	private UrlParameterNames(String parameterName) {
+		this.parameterName = parameterName;
+	}
+	
+	/**
+	 * @return the URL parameters name
+	 */
+	public String getParameterName() {
+		return parameterName;
+	}
 	
-	private UrlParameterNames() {
+	/**
+	 * Retrieves the UrlParameterName for the given name.
+	 * 
+	 * @param name the name to retrieve a URL parameter for
+	 * @return the URL parameter name if it exists, or null if no match is found
+	 */
+	public static UrlParameterNames getByParameterName(String name) {
+		for (UrlParameterNames param: values()) {
+			if (param.getParameterName().equalsIgnoreCase(name)) {
+				return param;
+			}
+		}
+		return null;
 	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java b/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java
index 636c40b5e8cdd6fb6cd35a5cd8142c799f1651e3..7a251fbbaca65da10e787ee29c590fd19c20aef2 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java
@@ -39,7 +39,7 @@ public class CacheBypassFilter implements ContainerRequestFilter {
 	@Override
 	public void filter(ContainerRequestContext requestContext) throws IOException {
 		// check for random sort order, which always bypasses cache
-		String[] sortVals = request.getParameterValues(UrlParameterNames.SORT);
+		String[] sortVals = request.getParameterValues(UrlParameterNames.SORT.getParameterName());
 		if (sortVals != null) {
 			for (String sortVal : sortVals) {
 				// check if the sort order for request matches RANDOM
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java
index 03454b235b75625765b18510c3eeedfdc396cd36..87aa67bfd980ce0d5333638a7b7ec6f6db5a29c2 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java
@@ -68,7 +68,7 @@ public class CatalogResource {
 		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
 		Optional<List<Catalog>> cachedResults = cachingService.get("all", params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached Catalogs");
 			return Response.serverError().build();
@@ -88,7 +88,7 @@ public class CatalogResource {
 	@RolesAllowed({ "marketplace_catalog_put", "marketplace_admin_access" })
 	public Response putCatalog(Catalog catalog) {
 		if (catalog.getId() != null) {
-			params.addParam(UrlParameterNames.ID, catalog.getId());
+			params.addParam(UrlParameterNames.ID.getParameterName(), catalog.getId());
 		}
 		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
 		// add the object, and await the result
@@ -108,12 +108,12 @@ public class CatalogResource {
 	@GET
 	@Path("/{catalogId}")
 	public Response select(@PathParam("catalogId") String catalogId) {
-		params.addParam(UrlParameterNames.ID, catalogId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), catalogId);
 
 		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
 		Optional<List<Catalog>> cachedResults = cachingService.get(catalogId, params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", catalogId);
 			return Response.serverError().build();
@@ -134,7 +134,7 @@ public class CatalogResource {
 	@RolesAllowed({ "marketplace_catalog_delete", "marketplace_admin_access" })
 	@Path("/{catalogId}")
 	public Response delete(@PathParam("catalogId") String catalogId) {
-		params.addParam(UrlParameterNames.ID, catalogId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), catalogId);
 
 		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
 		// delete the currently selected asset
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java
index 416acc1e38b4f066295e0a70e3b6607f0aa550e8..f671ea1ba141e4ae536d637de9842ccb5c686e8f 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java
@@ -68,7 +68,7 @@ public class CategoryResource {
 		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
 		Optional<List<Category>> cachedResults = cachingService.get("all", params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached Categorys");
 			return Response.serverError().build();
@@ -88,7 +88,7 @@ public class CategoryResource {
 	@RolesAllowed({"marketplace_category_put", "marketplace_admin_access"})
 	public Response putCategory(Category category) {
 		if (category.getId() != null) {
-			params.addParam(UrlParameterNames.ID, category.getId());
+			params.addParam(UrlParameterNames.ID.getParameterName(), category.getId());
 		}
 		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
 		// add the object, and await the result
@@ -108,12 +108,12 @@ public class CategoryResource {
 	@GET
 	@Path("/{categoryId}")
 	public Response select(@PathParam("categoryId") String categoryId) {
-		params.addParam(UrlParameterNames.ID, categoryId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), categoryId);
 
 		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
 		Optional<List<Category>> cachedResults = cachingService.get(categoryId, params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", categoryId);
 			return Response.serverError().build();
@@ -134,7 +134,7 @@ public class CategoryResource {
 	@RolesAllowed({ "marketplace_category_delete", "marketplace_admin_access" })
 	@Path("/{categoryId}")
 	public Response delete(@PathParam("categoryId") String categoryId) {
-		params.addParam(UrlParameterNames.ID, categoryId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), categoryId);
 
 		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
 		// delete the currently selected asset
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java
index 7d4c2f0ba330b387a0fd27118497357ef4cb949a..a596dddae908b35968f0b105ff7c2a5c12437e85 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java
@@ -70,7 +70,7 @@ public class ErrorReportResource {
 		MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
 		Optional<List<ErrorReport>> cachedResults = cachingService.get("all", params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached ErrorReports");
 			return Response.serverError().build();
@@ -91,7 +91,7 @@ public class ErrorReportResource {
 	public Response putErrorReport(ErrorReport errorReport) {
 		// attach ID if present for update
 		if (errorReport.getId() != null) {
-			params.addParam(UrlParameterNames.ID, errorReport.getId());
+			params.addParam(UrlParameterNames.ID.getParameterName(), errorReport.getId());
 		}
 		MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter);
 
@@ -113,12 +113,12 @@ public class ErrorReportResource {
 	@PermitAll
 	@Path("/{errorReportId}")
 	public Response select(@PathParam("errorReportId") String errorReportId) {
-		params.addParam(UrlParameterNames.ID, errorReportId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), errorReportId);
 
 		MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current ErrorReport
 		Optional<List<ErrorReport>> cachedResults = cachingService.get(errorReportId, params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached ErrorReport for ID {}", errorReportId);
 			return Response.serverError().build();
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java
index c49d8023e4d807e7c499497fff24c77fba16d75a..19aa49c1af412909720c76ce8a198686e29c5135 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java
@@ -92,10 +92,10 @@ public class InstallResource {
 	@PermitAll
 	@Path("/{listingId}")
 	public Response selectInstallCount(@PathParam("listingId") String listingId) {
-		wrapper.addParam(UrlParameterNames.ID, listingId);
+		wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId);
 		MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter);
 		Optional<Long> cachedResults = countCache.get(listingId, wrapper,
-				() -> StreamHelper.awaitCompletionStage(dao.count(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.count(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached install metrics for ID {}", listingId);
 			return Response.serverError().build();
@@ -118,11 +118,11 @@ public class InstallResource {
 	@PermitAll
 	@Path("/{listingId}/{version}")
 	public Response selectInstallCount(@PathParam("listingId") String listingId, @PathParam("version") String version) {
-		wrapper.addParam(UrlParameterNames.ID, listingId);
-		wrapper.addParam(UrlParameterNames.VERSION, version);
+		wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId);
+		wrapper.addParam(UrlParameterNames.VERSION.getParameterName(), version);
 		MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter);
 		Optional<Long> cachedResults = countCache.get(getCompositeKey(listingId, version), wrapper,
-				() -> StreamHelper.awaitCompletionStage(dao.count(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.count(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", listingId);
 			return Response.serverError().build();
@@ -143,10 +143,10 @@ public class InstallResource {
 	@PermitAll
 	@Path("/{listingId}/metrics")
 	public Response selectInstallMetrics(@PathParam("listingId") String listingId) {
-		wrapper.addParam(UrlParameterNames.ID, listingId);
+		wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId);
 		MongoQuery<InstallMetrics> q = new MongoQuery<>(wrapper, metricFilter);
 		Optional<List<InstallMetrics>> cachedResults = installCache.get(listingId, wrapper,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached install metrics for ID {}", listingId);
 			return Response.serverError().build();
@@ -240,8 +240,8 @@ public class InstallResource {
 			String end = DateTimeHelper.toRFC3339(c.getTime());
 			c.add(Calendar.MONTH, -1);
 			String start = DateTimeHelper.toRFC3339(c.getTime());
-			wrapper.setParam(UrlParameterNames.END, end);
-			wrapper.setParam(UrlParameterNames.START, start);
+			wrapper.setParam(UrlParameterNames.END.getParameterName(), end);
+			wrapper.setParam(UrlParameterNames.START.getParameterName(), start);
 
 			// create the query wrapper to pass to DB dao. No cache needed as this info
 			// won't be cached
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java
index fa5cd87ec89574c8113487214cd867c19348c3a8..1674ff47e03a47ec16691ade77e502539b7f3fb1 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java
@@ -9,6 +9,7 @@
 */
 package org.eclipsefoundation.marketplace.resource;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
@@ -35,8 +36,10 @@ import org.eclipsefoundation.marketplace.helper.StreamHelper;
 import org.eclipsefoundation.marketplace.model.Error;
 import org.eclipsefoundation.marketplace.model.MongoQuery;
 import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.model.SortOrder;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 import org.eclipsefoundation.marketplace.service.CachingService;
+import org.eclipsefoundation.marketplace.service.PromotionService;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -58,16 +61,20 @@ public class ListingResource {
 	@Inject
 	MongoDao dao;
 	@Inject
+	DtoFilter<Listing> dtoFilter;
+	@Inject
 	CachingService<List<Listing>> cachingService;
+
 	@Inject
-	RequestWrapper params;
+	PromotionService promoService;
+
 	@Inject
-	DtoFilter<Listing> dtoFilter;
+	RequestWrapper params;
 	@Inject
 	ResponseHelper responseBuider;
 
 	/**
-	 * Endpoint for /listing/ to retrieve all listings from the database along with
+	 * Endpoint for /listings/ to retrieve all listings from the database along with
 	 * the given query string parameters.
 	 * 
 	 * @param listingId int version of the listing ID
@@ -78,19 +85,36 @@ public class ListingResource {
 	public Response select() {
 		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<Listing>> cachedResults = cachingService.get("all", params,
+		Optional<List<Listing>> cachedResults = cachingService.get("all", params, null,
 				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listings");
 			return Response.serverError().build();
 		}
+		// make a copy to inject promotions and not affect cached copies
+		List<Listing> listings = new ArrayList<>(cachedResults.get());
+
+		// check if promotions should be injected
+		List<UrlParameterNames> active = params.getActiveParameters();
+		Optional<String> pageOpt = params.getFirstParam(UrlParameterNames.PAGE);
+		Optional<String> sortOpt = params.getFirstParam(UrlParameterNames.SORT);
+		if (active.stream().anyMatch(p -> !UrlParameterNames.PAGE.equals(p) && !UrlParameterNames.SORT.equals(p))) {
+			LOGGER.debug("Not injecting promotions, only '{}' and '{}' are allowed. Passed: {}",
+					UrlParameterNames.PAGE.getParameterName(), UrlParameterNames.SORT.getParameterName(), active);
+		} else if (pageOpt.isPresent() && !pageOpt.get().equals("1")) {
+			LOGGER.debug("Not injecting promotions, promotions are only injected on the first page");
+		} else if (sortOpt.isPresent() && !SortOrder.getOrderFromValue(sortOpt.get()).equals(SortOrder.RANDOM)) {
+			LOGGER.debug("Not injecting promotions, promotions are only injected in unsorted results");
+		} else {
+			listings = promoService.retrievePromotions(params, listings);
+		}
 
 		// return the results as a response
-		return responseBuider.build("all", params, cachedResults.get());
+		return responseBuider.build("all", params, listings);
 	}
 
 	/**
-	 * Endpoint for /listing/ to post a new listing to the persistence layer.
+	 * Endpoint for /listings/ to post a new listing to the persistence layer.
 	 * 
 	 * @param listing the listing object to insert into the database.
 	 * @return response for the browser
@@ -99,7 +123,7 @@ public class ListingResource {
 	@RolesAllowed({ "marketplace_listing_put", "marketplace_admin_access" })
 	public Response putListing(Listing listing) {
 		if (listing.getId() != null) {
-			params.addParam(UrlParameterNames.ID, listing.getId());
+			params.addParam(UrlParameterNames.ID.getParameterName(), listing.getId());
 		}
 		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
 
@@ -111,7 +135,7 @@ public class ListingResource {
 	}
 
 	/**
-	 * Endpoint for /listing/\<listingId\> to retrieve a specific listing from the
+	 * Endpoint for /listings/\<listingId\> to retrieve a specific listing from the
 	 * database.
 	 * 
 	 * @param listingId the listing ID
@@ -121,11 +145,11 @@ public class ListingResource {
 	@PermitAll
 	@Path("/{listingId}")
 	public Response select(@PathParam("listingId") String listingId) {
-		params.addParam(UrlParameterNames.ID, listingId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), listingId);
 
 		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
-		Optional<List<Listing>> cachedResults = cachingService.get(listingId, params,
+		Optional<List<Listing>> cachedResults = cachingService.get(listingId, params, null,
 				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", listingId);
@@ -137,7 +161,7 @@ public class ListingResource {
 	}
 
 	/**
-	 * Endpoint for /listing/\<listingId\> to delete a specific listing from the
+	 * Endpoint for /listings/\<listingId\> to delete a specific listing from the
 	 * database.
 	 * 
 	 * @param listingId the listing ID
@@ -147,7 +171,7 @@ public class ListingResource {
 	@RolesAllowed({ "marketplace_listing_delete", "marketplace_admin_access" })
 	@Path("/{listingId}")
 	public Response delete(@PathParam("listingId") String listingId) {
-		params.addParam(UrlParameterNames.ID, listingId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), listingId);
 		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
 		// delete the currently selected asset
 		DeleteResult result = StreamHelper.awaitCompletionStage(dao.delete(q));
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java
index 66f318ad967d8e13517d70f30c0d315a987997c2..1d312b8b373f794001f3d2a85342fce3e8a0f143 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java
@@ -68,7 +68,7 @@ public class ListingVersionResource {
 		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
 		Optional<List<ListingVersion>> cachedResults = cachingService.get("all", params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached ListingVersions");
 			return Response.serverError().build();
@@ -85,9 +85,10 @@ public class ListingVersionResource {
 	 * @return response for the browser
 	 */
 	@PUT
+	@RolesAllowed({ "marketplace_version_put", "marketplace_admin_access" })
 	public Response putListingVersion(ListingVersion listingVersion) {
 		if (listingVersion.getId() != null) {
-			params.addParam(UrlParameterNames.ID, listingVersion.getId());
+			params.addParam(UrlParameterNames.ID.getParameterName(), listingVersion.getId());
 		}
 		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
 		// add the object, and await the result
@@ -107,12 +108,12 @@ public class ListingVersionResource {
 	@GET
 	@Path("/{listingVersionId}")
 	public Response select(@PathParam("listingVersionId") String listingVersionId) {
-		params.addParam(UrlParameterNames.ID, listingVersionId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), listingVersionId);
 
 		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
 		Optional<List<ListingVersion>> cachedResults = cachingService.get(listingVersionId, params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", listingVersionId);
 			return Response.serverError().build();
@@ -133,7 +134,7 @@ public class ListingVersionResource {
 	@RolesAllowed({ "marketplace_version_delete", "marketplace_admin_access" })
 	@Path("/{listingVersionId}")
 	public Response delete(@PathParam("listingVersionId") String listingVersionId) {
-		params.addParam(UrlParameterNames.ID, listingVersionId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), listingVersionId);
 
 		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
 		// delete the currently selected asset
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java
index aa0fd9593b5f838c366dc087bcaceb4e5f676b34..24110f5b82a53376bb047bf7ca93054c601acfca 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java
@@ -68,7 +68,7 @@ public class MarketResource {
 		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
 		Optional<List<Market>> cachedResults = cachingService.get("all", params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached Categorys");
 			return Response.serverError().build();
@@ -88,7 +88,7 @@ public class MarketResource {
 	@RolesAllowed({ "marketplace_market_put", "marketplace_admin_access" })
 	public Response putMarket(Market market) {
 		if (market.getId() != null) {
-			params.addParam(UrlParameterNames.ID, market.getId());
+			params.addParam(UrlParameterNames.ID.getParameterName(), market.getId());
 		}
 		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
 
@@ -110,12 +110,12 @@ public class MarketResource {
 	@PermitAll
 	@Path("/{marketId}")
 	public Response select(@PathParam("marketId") String marketId) {
-		params.addParam(UrlParameterNames.ID, marketId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), marketId);
 
 		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
 		Optional<List<Market>> cachedResults = cachingService.get(marketId, params,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", marketId);
 			return Response.serverError().build();
@@ -133,9 +133,10 @@ public class MarketResource {
 	 * @return response for the browser
 	 */
 	@DELETE
+	@RolesAllowed({ "marketplace_market_delete", "marketplace_admin_access" })
 	@Path("/{marketId}")
 	public Response delete(@PathParam("marketId") String marketId) {
-		params.addParam(UrlParameterNames.ID, marketId);
+		params.addParam(UrlParameterNames.ID.getParameterName(), marketId);
 
 		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
 		// delete the currently selected asset
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cd8b03c25ce47f4402eaf5e777674917bda0f49
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java
@@ -0,0 +1,152 @@
+package org.eclipsefoundation.marketplace.resource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import javax.annotation.security.RolesAllowed;
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.eclipsefoundation.marketplace.dao.MongoDao;
+import org.eclipsefoundation.marketplace.dto.Promotion;
+import org.eclipsefoundation.marketplace.dto.filter.DtoFilter;
+import org.eclipsefoundation.marketplace.helper.ResponseHelper;
+import org.eclipsefoundation.marketplace.helper.StreamHelper;
+import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.marketplace.model.MongoQuery;
+import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.marketplace.service.CachingService;
+import org.jboss.resteasy.annotations.jaxrs.PathParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.mongodb.client.result.DeleteResult;
+
+/**
+ * Resource for interacting with promotions within the API.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Path("/promotions")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@RequestScoped
+public class PromotionResource {
+	private static final Logger LOGGER = LoggerFactory.getLogger(PromotionResource.class);
+
+	@Inject
+	MongoDao dao;
+	@Inject
+	DtoFilter<Promotion> dtoFilter;
+	@Inject
+	CachingService<List<Promotion>> cachingService;
+
+	@Inject
+	RequestWrapper params;
+	@Inject
+	ResponseHelper responseBuider;
+
+	/**
+	 * Endpoint for /promotions/ to retrieve all promotions from the database along
+	 * with the given query string parameters.
+	 * 
+	 * @return response for the browser with requested data, or an error response
+	 */
+	@GET
+	@RolesAllowed({ "marketplace_promotion_get", "marketplace_admin_access" })
+	public Response select() {
+		MongoQuery<Promotion> q = new MongoQuery<>(params, dtoFilter);
+		// retrieve the possible cached object
+		Optional<List<Promotion>> cachedResults = cachingService.get("all", params, Collections.emptyMap(),
+				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		if (!cachedResults.isPresent()) {
+			LOGGER.error("Error while retrieving cached promotions");
+			return Response.serverError().build();
+		}
+
+		// return the results as a response
+		return responseBuider.build("all", params, cachedResults.get());
+	}
+
+	/**
+	 * Endpoint for /promotions/ to post a new promotion to the persistence layer.
+	 * 
+	 * @param promotion the promotion object to insert into the database.
+	 * @return response for the browser with requested data, or an error response
+	 */
+	@PUT
+	@RolesAllowed({ "marketplace_promotion_put", "marketplace_admin_access" })
+	public Response putPromotion(Promotion promotion) {
+		if (promotion.getId() != null) {
+			params.addParam(UrlParameterNames.ID.getParameterName(), promotion.getId());
+		}
+		MongoQuery<Promotion> q = new MongoQuery<>(params, null, dtoFilter);
+		// add the object, and await the result
+		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(promotion)));
+
+		// return the results as a response
+		return Response.ok().build();
+	}
+
+	/**
+	 * Endpoint for /promotions/\<promotionId\> to retrieve a specific promotion
+	 * from the database.
+	 * 
+	 * @param promotionId the promotion ID
+	 * @return response for the browser with requested data, or an error response
+	 */
+	@GET
+	@RolesAllowed({ "marketplace_promotion_get", "marketplace_admin_access" })
+	@Path("/{promotionId}")
+	public Response select(@PathParam("promotionId") String promotionId) {
+		params.addParam(UrlParameterNames.ID.getParameterName(), promotionId);
+
+		MongoQuery<Promotion> q = new MongoQuery<>(params, null, dtoFilter);
+		// retrieve a cached version of the value for the current listing
+		Optional<List<Promotion>> cachedResults = cachingService.get(promotionId, params, Collections.emptyMap(),
+				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		if (!cachedResults.isPresent()) {
+			LOGGER.error("Error while retrieving cached listing for ID {}", promotionId);
+			return Response.serverError().build();
+		}
+
+		// return the results as a response
+		return responseBuider.build(promotionId, params, cachedResults.get());
+	}
+
+	/**
+	 * Endpoint for /promotions/\<promotionId\> to retrieve a specific promotion
+	 * from the database.
+	 * 
+	 * @param promotionId the promotion ID
+	 * @return response for the browser with requested data, or an error response
+	 */
+	@DELETE
+	@RolesAllowed({ "marketplace_promotion_delete", "marketplace_admin_access" })
+	@Path("/{promotionId}")
+	public Response delete(@PathParam("promotionId") String promotionId) {
+		params.addParam(UrlParameterNames.ID.getParameterName(), promotionId);
+
+		MongoQuery<Promotion> q = new MongoQuery<>(params, null, dtoFilter);
+		// delete the currently selected asset
+		DeleteResult result = StreamHelper.awaitCompletionStage(dao.delete(q));
+		if (result.getDeletedCount() == 0 || !result.wasAcknowledged()) {
+			return new Error(Status.NOT_FOUND, "Did not find an asset to delete for current call").asResponse();
+		}
+		// return the results as a response
+		return Response.ok().build();
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java b/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java
index 4821cab5aa58db293363723caaef12504f1901ae..5c7fc58bcb7199b858d04777743c6140840b1da0 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java
@@ -6,6 +6,8 @@
  */
 package org.eclipsefoundation.marketplace.service;
 
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
@@ -27,11 +29,13 @@ public interface CachingService<T> {
 	 * and returned.
 	 * 
 	 * @param id       the ID of the object to be stored in cache
-	 * @param params   the query parameters for the current request
+	 * @param wrapper  the query parameters for the current request
+	 * @param params   parameters to use in place of wrapper parameters when set
 	 * @param callable a runnable that returns an object of type T
 	 * @return the cached result
 	 */
-	Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable);
+	Optional<T> get(String id, RequestWrapper wrapper, Map<String, List<String>> params,
+			Callable<? extends T> callable);
 
 	/**
 	 * Returns the expiration date in millis since epoch.
@@ -73,16 +77,19 @@ public interface CachingService<T> {
 	 * 
 	 * @param id      identity string of the item to cache
 	 * @param wrapper parameters associated with the request for information
+	 * @param params  parameters to use in place of wrapper parameters when set
 	 * @return the unique cache key for the request.
 	 */
-	default String getCacheKey(String id, RequestWrapper wrapper) {
+	default String getCacheKey(String id, RequestWrapper wrapper, Map<String, List<String>> params) {
 		StringBuilder sb = new StringBuilder();
 		sb.append('[').append(wrapper.getEndpoint()).append(']');
 		sb.append("id:").append(id);
 
+		// get the used set of parameters for filtering data
+		Map<String, List<String>> actual = params == null ? wrapper.asMap() : params;
 		// join all the non-empty params to the key to create distinct entries for
 		// filtered values
-		wrapper.asMap().entrySet().stream().filter(e -> !e.getValue().isEmpty())
+		actual.entrySet().stream().filter(e -> !e.getValue().isEmpty())
 				.map(e -> e.getKey() + '=' + StringUtils.join(e.getValue(), ','))
 				.forEach(s -> sb.append('|').append(s));
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java b/src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d82f0947c8a0e361818183b0613a63714179779
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java
@@ -0,0 +1,37 @@
+package org.eclipsefoundation.marketplace.service;
+
+import java.util.List;
+
+import org.eclipsefoundation.marketplace.dto.Listing;
+import org.eclipsefoundation.marketplace.dto.Promotion;
+import org.eclipsefoundation.marketplace.model.RequestWrapper;
+
+/**
+ * Interface for retrieving promotions within the application.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public interface PromotionService {
+
+	/**
+	 * Retrieves listings associated with the given list of promotions
+	 * 
+	 * @param wrapper wrapper for the current request
+	 * @param promos  list of promotions to retrieve listings for
+	 * @return a list of listings for the list of promos, where data could be found,
+	 *         or an empty list if no corresponding listings could be found for the
+	 *         passed promotions.
+	 */
+	List<Listing> getListingsForPromotions(RequestWrapper wrapper, List<Promotion> promos);
+
+	/**
+	 * Adds a number of promotions into the given listing set.
+	 * 
+	 * @param wrapper  wrapper for the current request
+	 * @param listings listings to inject promotions into
+	 * @return a list containing the new promotions, if any are found, along with
+	 *         the original listings.
+	 */
+	List<Listing> retrievePromotions(RequestWrapper wrapper, List<Listing> listings);
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java b/src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8f8ab60a940bcff2e0a4e037f31dc6b91f2eafb
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java
@@ -0,0 +1,173 @@
+package org.eclipsefoundation.marketplace.service.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipsefoundation.marketplace.dao.MongoDao;
+import org.eclipsefoundation.marketplace.dto.Listing;
+import org.eclipsefoundation.marketplace.dto.Promotion;
+import org.eclipsefoundation.marketplace.dto.filter.DtoFilter;
+import org.eclipsefoundation.marketplace.helper.StreamHelper;
+import org.eclipsefoundation.marketplace.model.MongoQuery;
+import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.marketplace.service.CachingService;
+import org.eclipsefoundation.marketplace.service.PromotionService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of promotion service. Uses weighting to allow for
+ * promotions to appear more often than others. By using the property
+ * {@link MicroprofilePropertyNames.PROMO_WEIGHT_DEFAULT}, weighting defaults
+ * can shift outside of code builds once data is modified.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@ApplicationScoped
+public class DefaultPromotionService implements PromotionService {
+	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPromotionService.class);
+
+	@ConfigProperty(name = MicroprofilePropertyNames.PROMO_SERVE_COUNT, defaultValue = "2")
+	int promoCount;
+	@ConfigProperty(name = MicroprofilePropertyNames.PROMO_WEIGHT_DEFAULT, defaultValue = "1")
+	int defaultWeight;
+
+	@Inject
+	MongoDao dao;
+
+	@Inject
+	DtoFilter<Listing> listingFilter;
+	@Inject
+	CachingService<List<Listing>> listingCache;
+
+	@Inject
+	DtoFilter<Promotion> promotionFilter;
+	@Inject
+	CachingService<List<Promotion>> promoCache;
+
+	// random used for shuffling collections
+	private Random r = new Random();
+
+	@Override
+	public List<Listing> getListingsForPromotions(RequestWrapper wrapper, List<Promotion> promos) {
+		if (promos == null || promos.isEmpty()) {
+			LOGGER.debug("No promotions were passed, returning empty list");
+			return Collections.emptyList();
+		}
+		// create mapping to get a list of specific IDs, and to add context to the call
+		// for caching
+		Map<String, List<String>> adds = new HashMap<>();
+		adds.put("type", Arrays.asList("Listing"));
+		adds.put(UrlParameterNames.IDS.getParameterName(),
+				promos.stream().map(Promotion::getListingId).collect(Collectors.toList()));
+
+		MongoQuery<Listing> q = new MongoQuery<>(null, adds, listingFilter);
+		// retrieve the possible cached object
+		Optional<List<Listing>> cachedResults = listingCache.get("promo|listings", wrapper, adds,
+				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		if (!cachedResults.isPresent()) {
+			LOGGER.error("Error while retrieving cached promotion listings");
+			return Collections.emptyList();
+		}
+
+		// return the results as a response
+		return cachedResults.get();
+	}
+
+	@Override
+	public List<Listing> retrievePromotions(RequestWrapper wrapper, List<Listing> listings) {
+		// create an empty promo query to get all promos
+		MongoQuery<Promotion> q = new MongoQuery<>(null, Collections.emptyMap(), promotionFilter);
+		// retrieve the possible cached object
+		Optional<List<Promotion>> cachedResults = promoCache.get("all|promo", wrapper, Collections.emptyMap(),
+				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		if (!cachedResults.isPresent() || cachedResults.get().isEmpty()) {
+			LOGGER.debug("Could not find any promotions to inject, returning");
+			return listings;
+		}
+		// make a copy of the array to not impact cached values
+		List<Promotion> promos = new ArrayList<>(cachedResults.get());
+		List<Promotion> promoHolding = new ArrayList<>(promoCount);
+		LOGGER.debug("Found {} promotions, maximum number to inject {}", promos.size(), promoCount);
+		// check each promotion to see if it should be injected
+		Promotion curr = getWeightedPromotion(promos);
+		if (curr != null) {
+			promos.remove(curr);
+		}
+		while (curr != null && promoHolding.size() <= promoCount) {
+			// create a local final field referencing the current promotion for stream ref
+			final Promotion p = curr;
+			LOGGER.debug("Checking promo {}", p.getListingId());
+			// check if current promo matches any of the listing IDs
+			if (promos.stream().noneMatch(l -> l.getId().equals(p.getListingId()))) {
+				LOGGER.debug("Preparing promo with listing ID '{}' to be injected into result set",
+						curr.getListingId());
+
+				promoHolding.add(curr);
+			}
+			curr = getWeightedPromotion(promos);
+			if (curr != null) {
+				promos.remove(curr);
+			}
+		}
+
+		// if we couldn't find enough promos, reinsert records
+		List<Listing> out = new ArrayList<>(listings);
+		if (promoHolding.isEmpty()) {
+			LOGGER.debug("Could not find any promos to inject");
+		} else {
+			for (Listing listing : getListingsForPromotions(wrapper, promoHolding)) {
+				LOGGER.debug("Injecting promo with listing ID '{}' ", listing.getId());
+				listing.setPromotion(true);
+				out.add(0, listing);
+			}
+		}
+		return out;
+	}
+
+	/**
+	 * Using the weighting set in the promotions (or default if not set), retrieve a
+	 * random result, taking weighting into account.
+	 * 
+	 * @param promos list of promotions to retrieve a result from. This list will be
+	 *               modified as part of this call to shuffle and pop the chosen
+	 *               entry.
+	 * @return the chosen weighted and randomized promotion, or null if none are
+	 *         appropriate.
+	 */
+	private Promotion getWeightedPromotion(List<Promotion> promos) {
+		// return if there are no promotions to choose from
+		if (promos.isEmpty()) {
+			return null;
+		}
+		int totalWeighting = promos.stream().mapToInt(Promotion::getWeight).sum();
+		// get a random number in the range of the total weighting
+		int rnd = r.nextInt(totalWeighting);
+		Promotion result = null;
+		for (Promotion p: promos) {
+			// reduce the random number by the weight
+			rnd -= p.getWeight();
+			// check if we are in range of the current entry
+			if (rnd <= 0) {
+				result = p;
+				break;
+			}
+		}
+		return result;
+	}
+
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java b/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java
index d3940a77d970a8c9925dfa25fe2662b68c483dc3..6addf934f7f8b821c57178228dd88970d8ce2a3a 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java
@@ -7,6 +7,7 @@
 package org.eclipsefoundation.marketplace.service.impl;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -19,6 +20,7 @@ import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames;
 import org.eclipsefoundation.marketplace.service.CachingService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,9 +50,9 @@ import com.google.common.util.concurrent.UncheckedExecutionException;
 public class GuavaCachingService<T> implements CachingService<T> {
 	private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCachingService.class);
 
-	@ConfigProperty(name = "cache.max.size", defaultValue = "10000")
+	@ConfigProperty(name = MicroprofilePropertyNames.CACHE_SIZE_MAX, defaultValue = "10000")
 	long maxSize;
-	@ConfigProperty(name = "cache.ttl.write.seconds", defaultValue = "900")
+	@ConfigProperty(name = MicroprofilePropertyNames.CACHE_TTL_MAX_SECONDS, defaultValue = "900")
 	long ttlWrite;
 
 	// actual cache object
@@ -71,15 +73,17 @@ public class GuavaCachingService<T> implements CachingService<T> {
 	}
 
 	@Override
-	public Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable) {
+	public Optional<T> get(String id, RequestWrapper wrapper, Map<String, List<String>> params,
+			Callable<? extends T> callable) {
 		Objects.requireNonNull(id);
-		Objects.requireNonNull(params);
+		Objects.requireNonNull(wrapper);
 		Objects.requireNonNull(callable);
 
-		String cacheKey = getCacheKey(id, params);
+		String cacheKey = getCacheKey(id, wrapper, params);
+		LOGGER.debug("Retrieving cache value for '{}'", cacheKey);
 		try {
 			// check if the cache is bypassed for the request
-			if (params.isCacheBypass()) {
+			if (wrapper.isCacheBypass()) {
 				T result = callable.call();
 				// if the cache has a value for key, update it
 				if (cache.asMap().containsKey(cacheKey)) {
@@ -104,7 +108,7 @@ public class GuavaCachingService<T> implements CachingService<T> {
 
 	@Override
 	public Optional<Long> getExpiration(String id, RequestWrapper params) {
-		return Optional.ofNullable(ttl.get(getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params))));
+		return Optional.ofNullable(ttl.get(getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params), null)));
 	}
 	
 	@Override
@@ -126,4 +130,5 @@ public class GuavaCachingService<T> implements CachingService<T> {
 	public long getMaxAge() {
 		return ttlWrite;
 	}
+
 }
diff --git a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java
index f6b07e61c51c3070a94cd7941b60c3ba69ce3329..41d65df20003061de18bcdd6534a2750422078dd 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java
+++ b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java
@@ -9,20 +9,17 @@ package org.eclipsefoundation.marketplace.helper;
 import java.util.List;
 import java.util.Optional;
 
-import org.eclipsefoundation.marketplace.helper.SortableHelper;
 import org.eclipsefoundation.marketplace.helper.SortableHelper.Sortable;
 import org.eclipsefoundation.marketplace.model.SortableField;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-import io.quarkus.test.junit.DisabledOnSubstrate;
 import io.quarkus.test.junit.QuarkusTest;
 
 /**
  * @author Martin Lowe
  *
  */
-@DisabledOnSubstrate
 @QuarkusTest
 public class SortableHelperTest {
 
diff --git a/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java b/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java
index 798c1d7ef3e640eb1d876ab96eecc9124efa6dfb..197951446084e4abea7e9a0aa3e4c316ff63ef0e 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java
+++ b/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java
@@ -6,6 +6,7 @@
  */
 package org.eclipsefoundation.marketplace.service.impl;
 
+import java.util.Collections;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -60,18 +61,18 @@ public class GuavaCachingServiceTest {
 
 		// without post construct init via javax management, cache will not be properly
 		// set
-		Assertions.assertTrue(!gcsManual.get("sampleKey", sample, Object::new).isPresent(),
+		Assertions.assertTrue(!gcsManual.get("sampleKey", sample, Collections.emptyMap(), Object::new).isPresent(),
 				"Object should not be generated when there is no cache initialized");
 
 		// initialize the cache w/ configs
 		gcsManual.init();
 
 		// run a command to interact with cache
-		Assertions.assertTrue(gcsManual.get("sampleKey", sample, Object::new).isPresent(),
+		Assertions.assertTrue(gcsManual.get("sampleKey", sample, Collections.emptyMap(), Object::new).isPresent(),
 				"Object should be generated once cache is instantiated");
 
 		// test the injected cache service (which is the normal use case)
-		Assertions.assertTrue(gcs.get("sampleKey", sample, Object::new).isPresent(),
+		Assertions.assertTrue(gcs.get("sampleKey", sample, Collections.emptyMap(), Object::new).isPresent(),
 				"Object should be generated once cache is instantiated");
 	}
 
@@ -81,7 +82,7 @@ public class GuavaCachingServiceTest {
 		String key = "k";
 
 		// get the cached obj from a fresh cache
-		Optional<Object> cachedObj = gcs.get(key, sample, () -> cachableObject);
+		Optional<Object> cachedObj = gcs.get(key, sample, Collections.emptyMap(), () -> cachableObject);
 
 		Assertions.assertTrue(cachedObj.isPresent());
 		Assertions.assertEquals(cachableObject, cachedObj.get());
@@ -90,19 +91,19 @@ public class GuavaCachingServiceTest {
 	@Test
 	public void testGetNullCallable() {
 		Assertions.assertThrows(NullPointerException.class, () -> {
-			gcs.get("key", sample, null);
+			gcs.get("key", sample, Collections.emptyMap(), null);
 		});
 	}
 
 	@Test
 	public void testGetNullCallableResult() {
-		Optional<Object> emptyObj = gcs.get("failure key", sample, () -> null);
+		Optional<Object> emptyObj = gcs.get("failure key", sample, Collections.emptyMap(), () -> null);
 		Assertions.assertFalse(emptyObj.isPresent());
 	}
 
 	@Test
 	public void testGetExceptionalCallable() {
-		Optional<Object> emptyObj = gcs.get("k", sample, () -> {
+		Optional<Object> emptyObj = gcs.get("k", sample, Collections.emptyMap(), () -> {
 			throw new IllegalStateException();
 		});
 		Assertions.assertFalse(emptyObj.isPresent());
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 9c5f91e89329cd818f9bf6a1fb1d238b28cd365b..b4ffee9e0fa2c862efe1e81792ab78ded2dbaefa 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -9,10 +9,18 @@ quarkus.log.level=TRACE
 quarkus.log.file.path=/tmp/logs/quarkus.log
 
 ## DATASOURCE CONFIG
-quarkus.mongodb.connection-string=mongodb://192.168.1.178:27017
+quarkus.mongodb.connection-string = mongodb://localhost:27017
+quarkus.mongodb.credentials.username=root
+quarkus.mongodb.write-concern.safe=true
+quarkus.mongodb.min-pool-size=100
+quarkus.mongodb.max-pool-size=1000
+quarkus.mongodb.write-concern.retry-writes=true
 mongodb.database=mpc
 mongodb.default.limit=25
 mongodb.default.limit.max=100
 
+# MISC
+quarkus.resteasy.gzip.enabled=true
+
 # TEST PROPERTIES
 sample.secret.property=application-value
\ No newline at end of file