diff --git a/dict-ws/tdt4250.dict3.gogo/README.md b/dict-ws/tdt4250.dict3.gogo/README.md
index 009cae1ea873eb36343eba2a13e2a4176e8d30dc..fa5d9c7025ce9c88ffdc5b1b7a09d5e923a84d9c 100644
--- a/dict-ws/tdt4250.dict3.gogo/README.md
+++ b/dict-ws/tdt4250.dict3.gogo/README.md
@@ -14,4 +14,4 @@ A Gogo shell command component is an ordinary Java class annotated as a componen
 
 There are commands for listing dictionaries (all **Dict** service components), looking up words and adding and removing dictionaries, by registering or unregistering **Dict** service components. A custom **Dict** implementation is used for new dictionaries. These may load words from a file and include additional words.
 
-OSGI only allows unregistering components by using the **ServiceRegistration** that was returned when (manually) registering them. Hence, these must be stored by the **add** command and retrieved by the **remove** command. It turned out that the command component isn't a singleton, it is recreated for each invoked command, hence the **ServiceRegistration** must be stored somewhere else. For this a **BundleActivator** is used, i.e. a bundle singleton typically used for storing bundle global data.
+OSGI only allows manually unregistering components that have been manually created (for which you have a **ServiceReference** or **Configuration**), so you can only remove dictionaries that have been manually added.
diff --git a/dict-ws/tdt4250.dict3.gogo/bnd.bnd b/dict-ws/tdt4250.dict3.gogo/bnd.bnd
index c8278b1995232f505e06d4d24f1839cbf6ceb493..8fff410d72193da6fe94ded8e13a08b64091fbbc 100644
--- a/dict-ws/tdt4250.dict3.gogo/bnd.bnd
+++ b/dict-ws/tdt4250.dict3.gogo/bnd.bnd
@@ -14,5 +14,4 @@
 javac.source: 1.8
 javac.target: 1.8
 
-Bundle-Version: 0.0.0.${tstamp}
-Bundle-Activator: tdt4250.dict3.gogo.Activator
\ No newline at end of file
+Bundle-Version: 0.0.0.${tstamp}
\ No newline at end of file
diff --git a/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/Activator.java b/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/Activator.java
deleted file mode 100644
index 6ccb5c7fe007492756a09bd5ef273aa6c4f4ab5f..0000000000000000000000000000000000000000
--- a/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/Activator.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package tdt4250.dict3.gogo;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.osgi.framework.BundleActivator;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.FrameworkUtil;
-import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.framework.ServiceReference;
-import org.osgi.framework.ServiceRegistration;
-
-import tdt4250.dict3.api.Dict;
-
-public class Activator implements BundleActivator {
-
-	private static Activator SINGLETON = null;
-	
-	static Activator getActivator() {
-		return SINGLETON;
-	}
-	
-	@Override
-	public void start(BundleContext context) throws Exception {
-		SINGLETON = this;
-	}
-
-	@Override
-	public void stop(BundleContext context) throws Exception {
-		SINGLETON = null;
-	}
-	
-	private Map<String, ServiceRegistration<Dict>> serviceRegistrations = new HashMap<String, ServiceRegistration<Dict>>();
-
-	public boolean isManual(String dictName) {
-		return serviceRegistrations.containsKey(dictName);
-	}
-
-	public Collection<String> getAllDictComponentNames() {
-		Collection<String> allNames = new ArrayList<>();
-		// iterate through all Dict service objects
-		BundleContext bc = FrameworkUtil.getBundle(this.getClass()).getBundleContext();
-		try {
-			for (ServiceReference<Dict> serviceReference : bc.getServiceReferences(Dict.class, null)) {
-				Dict dict = bc.getService(serviceReference);
-				try {
-					allNames.add(dict.getDictName());
-				} finally {
-					bc.ungetService(serviceReference);
-				}
-			}
-		} catch (InvalidSyntaxException e) {
-		}
-		return allNames;
-	}
-	
-	public boolean addDict(Dict dict) {
-		boolean existed = removeDict(dict.getDictName());
-		BundleContext bc = FrameworkUtil.getBundle(this.getClass()).getBundleContext();
-		ServiceRegistration<Dict> serviceRegistration = bc.registerService(Dict.class, dict, null);
-		serviceRegistrations.put(dict.getDictName(), serviceRegistration);
-		return existed;
-	}
-	
-	public boolean removeDict(String name) {
-		if (serviceRegistrations.containsKey(name)) {
-			serviceRegistrations.get(name).unregister();
-			serviceRegistrations.remove(name);
-			return true;
-		}
-		return false;
-	}
-}
diff --git a/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/CommandDict.java b/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/CommandDict.java
deleted file mode 100644
index f8781d326be42ef681467d6c374d4720ebfabda6..0000000000000000000000000000000000000000
--- a/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/CommandDict.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package tdt4250.dict3.gogo;
-
-import tdt4250.dict3.api.Dict;
-import tdt4250.dict3.util.Words;
-import tdt4250.dict3.util.WordsDict;
-
-public class CommandDict extends WordsDict implements Dict {
-
-	private final String name;
-	
-	public CommandDict(String name, Words words) {
-		super(words);
-		this.name = name;
-	}
-
-	@Override
-	public String getDictName() {
-		return name;
-	}
-
-}
diff --git a/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/DictCommands.java b/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/DictCommands.java
index 6aaf2627028f50baeecee7aa90771431c07aef3f..dff6e1cee7659ff325fedbfffac333e33366561c 100644
--- a/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/DictCommands.java
+++ b/dict-ws/tdt4250.dict3.gogo/src/tdt4250/dict3/gogo/DictCommands.java
@@ -3,19 +3,26 @@ package tdt4250.dict3.gogo;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Hashtable;
 
 import org.apache.felix.service.command.Descriptor;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
 
 import tdt4250.dict3.api.Dict;
 import tdt4250.dict3.api.DictSearchResult;
-import tdt4250.dict3.util.MutableWords;
-import tdt4250.dict3.util.ResourceWords;
-import tdt4250.dict3.util.SortedSetWords;
+import tdt4250.dict3.util.WordsDict;
 
 // see https://enroute.osgi.org/FAQ/500-gogo.html
 
@@ -31,16 +38,37 @@ import tdt4250.dict3.util.SortedSetWords;
 	)
 public class DictCommands {
 
+	private Configuration getConfig(String dictName) {
+		try {
+			Configuration[] configs = cm.listConfigurations("(&(" + WordsDict.DICT_NAME_PROP + "=" + dictName + ")(service.factoryPid=" + WordsDict.FACTORY_PID + "))");
+			if (configs != null && configs.length >= 1) {
+				return configs[0];
+			}
+		} catch (IOException | InvalidSyntaxException e) {
+		}
+		return null;
+	}
+
 	@Descriptor("list available dictionaries")
 	public void list() {
-		Activator activator = Activator.getActivator();
 		System.out.print("Dictionaries: ");
-		for (String dictName : activator.getAllDictComponentNames()) {
-			System.out.print(dictName);
-			if (activator.isManual(dictName)) {
-				System.out.print("*");						
+		BundleContext bc = FrameworkUtil.getBundle(this.getClass()).getBundleContext();
+		try {
+			for (ServiceReference<Dict> serviceReference : bc.getServiceReferences(Dict.class, null)) {
+				Dict dict = bc.getService(serviceReference);
+				try {
+					if (dict != null) {
+						System.out.print(dict.getDictName());
+						if (getConfig(dict.getDictName()) != null) {
+							System.out.print("*");						
+						}
+					}
+				} finally {
+					bc.ungetService(serviceReference);
+				}
+				System.out.print(" ");
 			}
-			System.out.print(" ");
+		} catch (InvalidSyntaxException e) {
 		}
 		System.out.println();
 	}
@@ -55,17 +83,24 @@ public class DictCommands {
 			// iterate through all Dict service objects
 			for (ServiceReference<Dict> serviceReference : bc.getServiceReferences(Dict.class, null)) {
 				Dict dict = bc.getService(serviceReference);
-				try {
-					DictSearchResult search = dict.search(s);
-					System.out.println(dict.getDictName() + ": " + search.getMessage());
-				} finally {
-					bc.ungetService(serviceReference);
+				if (dict != null) {
+					try {
+						DictSearchResult search = dict.search(s);
+						System.out.println(dict.getDictName() + ": " + search.getMessage());
+					} finally {
+						bc.ungetService(serviceReference);
+					}
+				} else {
+					System.out.println(serviceReference.getProperties());
 				}
 			}
 		} catch (InvalidSyntaxException e) {
 		}
 	}
-	
+
+	@Reference(cardinality = ReferenceCardinality.MANDATORY)
+	private ConfigurationAdmin cm;
+
 	@Descriptor("add a dictionary, with content from a URL and/or specific words")
 	public void add(
 			@Descriptor("the name of the new dictionary")
@@ -74,33 +109,46 @@ public class DictCommands {
 			String urlStringOrWord,
 			@Descriptor("additional words to add to the dictionary")
 			String... ss
-			) {
-		MutableWords words = null;
+			) throws IOException, InvalidSyntaxException {
+		URL url = null;
+		Collection<String> words = new ArrayList<String>();
 		try {
-			URL url = new URL(urlStringOrWord);
-			try {
-				words = new ResourceWords(url.openStream());
-			} catch (IOException e) {
-				System.err.println("Couldn't read from " + url);
-				return;
-			}
+			url = new URL(urlStringOrWord);
 		} catch (MalformedURLException e) {
-			words = new SortedSetWords();
-			words.addWord(urlStringOrWord);
+			words.add(urlStringOrWord);
+		}
+		words.addAll(Arrays.asList(ss));
+		String actionName = "updated";
+		// lookup existing configuration
+		Configuration config = getConfig(name);
+		if (config == null) {
+			// create a new one
+			config = cm.createFactoryConfiguration(WordsDict.FACTORY_PID, "?");
+			actionName = "added";
+		}
+		Dictionary<String, String> props = new Hashtable<>();
+		props.put(WordsDict.DICT_NAME_PROP, name);
+		if (url != null) {
+			props.put(WordsDict.DICT_RESOURCE_PROP, url.toString());
 		}
-		for (String s : ss) {
-			words.addWord(s);
+		if (words != null && words.size() > 0) {
+			props.put(WordsDict.DICT_WORDS_PROP, String.join(" ", words));
 		}
-		boolean existed = Activator.getActivator().addDict(new CommandDict(name, words));
-		System.out.println("\"" + name + "\" dictionary " + (existed ? "replaced" : "added"));
+		config.update(props);
+		System.out.println("\"" + name + "\" dictionary " + actionName);
 	}
-	
+
 	@Descriptor("remove a (manually added) dictionary")
 	public void remove(
 			@Descriptor("the name of the (manually added) dictionary to remove")
 			String name
-			) {
-		boolean removed = Activator.getActivator().removeDict(name);
+			) throws IOException, InvalidSyntaxException {
+		Configuration config = getConfig(name);
+		boolean removed = false;
+		if (config != null) {
+			config.delete();
+			removed = true;
+		}
 		System.out.println("\"" + name + "\" dictionary " + (removed ? "removed" : "was not added manually"));
 	}
 }
diff --git a/dict-ws/tdt4250.dict3.no/src/tdt4250/dict3/no/NbDict.java b/dict-ws/tdt4250.dict3.no/src/tdt4250/dict3/no/NbDict.java
index 3cf2f0784f0342b0dfe21f804e4aa740219f964d..c9de058ee480b6397143b99e82c0870f16f588cb 100644
--- a/dict-ws/tdt4250.dict3.no/src/tdt4250/dict3/no/NbDict.java
+++ b/dict-ws/tdt4250.dict3.no/src/tdt4250/dict3/no/NbDict.java
@@ -1,21 +1,14 @@
 package tdt4250.dict3.no;
 
-import java.io.IOException;
-
 import org.osgi.service.component.annotations.Component;
 
 import tdt4250.dict3.api.Dict;
 import tdt4250.dict3.util.WordsDict;
 
-@Component
+@Component(
+		property = {
+				WordsDict.DICT_NAME_PROP + "=nb",
+				WordsDict.DICT_RESOURCE_PROP + "=tdt4250.dict3.no#/tdt4250/dict3/no/nb.txt"}
+		)
 public class NbDict extends WordsDict implements Dict {
-
-	public NbDict() throws IOException {
-		super(NbDict.class.getResourceAsStream("nb.txt"));		
-	}
-
-	@Override
-	public String getDictName() {
-		return "nb";
-	}
 }
diff --git a/dict-ws/tdt4250.dict3.servlet/launch.bndrun b/dict-ws/tdt4250.dict3.servlet/launch.bndrun
index c5e51e570982e87a8bb81b2a9363f98c6368c356..6cb822ace66519a43f4828ac0f77cc4e10b430fa 100644
--- a/dict-ws/tdt4250.dict3.servlet/launch.bndrun
+++ b/dict-ws/tdt4250.dict3.servlet/launch.bndrun
@@ -5,8 +5,9 @@
 -resolve.effective: active
 
 -runproperties: \
-	org.osgi.service.http.port=8080,\
+	osgi.clean=true,\
 	osgi.console=,\
+	org.osgi.service.http.port=8080,\
 	osgi.console.enable.builtin=false
 
 -runrequires: \
@@ -14,7 +15,9 @@
 	osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.command)',\
 	bnd.identity;id='tdt4250.dict3.servlet',\
 	bnd.identity;id='tdt4250.dict3.no',\
-	bnd.identity;id='tdt4250.dict3.gogo'
+	bnd.identity;id='tdt4250.dict3.gogo',\
+	bnd.identity;id='org.apache.felix.configadmin',\
+	bnd.identity;id='org.osgi.service.cm'
 -runbundles: \
 	org.apache.felix.gogo.command;version='[1.0.2,1.0.3)',\
 	org.apache.felix.gogo.runtime;version='[1.0.10,1.0.11)',\
@@ -26,4 +29,6 @@
 	tdt4250.dict3.servlet;version=snapshot,\
 	tdt4250.dict3.no;version=snapshot,\
 	tdt4250.dict3.util;version=snapshot,\
-	tdt4250.dict3.gogo;version=snapshot
\ No newline at end of file
+	tdt4250.dict3.gogo;version=snapshot,\
+	org.apache.felix.configadmin;version='[1.9.8,1.9.9)',\
+	org.osgi.service.cm;version='[1.5.0,1.5.1)'
\ No newline at end of file
diff --git a/dict-ws/tdt4250.dict3.util/README.md b/dict-ws/tdt4250.dict3.util/README.md
index 45a10a02d4d1eec0afefbd997f95d7d2f879bae5..8fb12d4f21cd6bba467f684441ba990c7170df07 100644
--- a/dict-ws/tdt4250.dict3.util/README.md
+++ b/dict-ws/tdt4250.dict3.util/README.md
@@ -1,7 +1,21 @@
 # tdt4250.dict3.no bundle
 
-This bundle is part of variant 3 of the [tdt4250.dict project](../README.md), and was created using the API template. It contains utility classes useful for dictionary implementations.
+This bundle is part of variant 3 of the [tdt4250.dict project](../README.md), and was created using the API template. It contains utility classes useful for dictionary implementations, as well as a **Dict** implementation that can be configured using the **ConfigAdmin** OSGi service.
 
 ## Packages
 
 - **tdt4250.dict3.no**: Utility classes useful for dictionary implementations.
+
+
+## Design
+
+The main class of this bundle is the **WordsDict** implementation of the **Dict** service. Both the name and the words can be configured using properties, and the words may be both loaded from a **URL** (or as a special case a resource in a bundle) or from a property. Thus, by creating a **Configuration** with the appropriate properties, a new **Dict** may be created and automatically injected into **Dict** service consumers.
+
+The **configurationPid** is **tdt4250.dict3.util.WordsDict**, i.e. the class name, and is used as the **factoryPid** when creating new **Configuration** objects.
+
+The properties are:
+- **dictName**: the name of the new dictionary
+- **dictResource**: the URL or location of a bundle resource (format <bundle>#<resource-path>, e.g. tdt4250.dict3.no#/tdt4250/dict3/no/nb.txt)that contains the words
+- **dictWords**: a space-separated list of the words
+
+Constants for these are defined in the **WordsDict** class.
diff --git a/dict-ws/tdt4250.dict3.util/bnd.bnd b/dict-ws/tdt4250.dict3.util/bnd.bnd
index 7849058f5fec812967dc004ef77c59ad250fdd41..80a80c0c641d32dcd92b96291b25aa26b566e705 100644
--- a/dict-ws/tdt4250.dict3.util/bnd.bnd
+++ b/dict-ws/tdt4250.dict3.util/bnd.bnd
@@ -3,7 +3,10 @@ Bundle-Version: 1.0.0.${tstamp}
 -baseline: *
 -buildpath: \
 	osgi.annotation;version=7.0.0,\
-	tdt4250.dict3.api;version=latest
+	tdt4250.dict3.api;version=latest,\
+	osgi.cmpn,\
+	org.osgi.service.cm,\
+	org.osgi.core
 
 javac.source: 1.8
 javac.target: 1.8
diff --git a/dict-ws/tdt4250.dict3.util/src/tdt4250/dict3/util/WordsDict.java b/dict-ws/tdt4250.dict3.util/src/tdt4250/dict3/util/WordsDict.java
index aa5e738024880c3a58566138d4dadd6ad9af03bd..61a00222e48d92ed1866fa7bb3ed871351c8983e 100644
--- a/dict-ws/tdt4250.dict3.util/src/tdt4250/dict3/util/WordsDict.java
+++ b/dict-ws/tdt4250.dict3.util/src/tdt4250/dict3/util/WordsDict.java
@@ -2,21 +2,109 @@ package tdt4250.dict3.util;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
 
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Modified;
+
+import tdt4250.dict3.api.Dict;
 import tdt4250.dict3.api.DictSearchResult;
 
-public abstract class WordsDict {
+@Component(
+		configurationPid = WordsDict.FACTORY_PID,
+		configurationPolicy = ConfigurationPolicy.REQUIRE
+		)
+public class WordsDict implements Dict {
 
+	public static final String FACTORY_PID = "tdt4250.dict3.util.WordsDict";
+	
+	public static final String DICT_WORDS_PROP = "dictWords";
+	public static final String DICT_RESOURCE_PROP = "dictResource";
+	public static final String DICT_NAME_PROP = "dictName";
+	
+	private String name;
 	private Words words;
 	
-	protected WordsDict(Words words) {
-		this.words = words;
+	@Override
+	public String getDictName() {
+		return name;
 	}
 
-	protected WordsDict(InputStream input) throws IOException {
-		this.words = new ResourceWords(input);
+	protected void setDictName(String name) {
+		this.name = name;
+	}
+	
+	public @interface WordsDictConfig {
+		String dictName();
+		String dictResource() default "";
+		String[] dictWords() default {};
 	}
 
+	@Activate
+	public void activate(BundleContext bc, WordsDictConfig config) {
+		update(bc, config);
+	}
+
+	@Modified
+	public void modify(BundleContext bc, WordsDictConfig config) {
+		update(bc, config);		
+	}
+
+	protected void update(BundleContext bc, WordsDictConfig config) {
+		setDictName(config.dictName());
+		String dictUrl = config.dictResource();
+		if (dictUrl.length() > 0) {
+			URL url = null;
+			try {
+				url = new URL(dictUrl);
+			} catch (MalformedURLException e) {
+				// try bundle resource format: <bundle-id>#<resource-path>
+				int pos = dictUrl.indexOf('#');
+				String bundleId = dictUrl.substring(0, pos);
+				String resourcePath = dictUrl.substring(pos + 1);
+				for (Bundle bundle : bc.getBundles()) {
+					if (bundle.getSymbolicName().equals(bundleId)) {
+						url = bundle.getResource(resourcePath);
+					}
+				}
+			}
+			try {
+				System.out.println("Loading words from " + url);
+				words = new ResourceWords(url.openStream());
+			} catch (IOException e) {
+				System.err.println(e);
+			}
+		}
+		if (config.dictWords().length > 0) {
+			String[] ss = config.dictWords();
+			if (words == null) {
+				words = new SortedSetWords();
+			}
+			if (words instanceof MutableWords) {
+				for (int i = 0; i < ss.length; i++) {
+					((MutableWords) words).addWord(ss[i].trim());
+				}
+			}
+		}
+	}
+
+	protected void setWords(Words words) {
+		this.words = words;
+	}
+	
+	protected void setWords(InputStream input) throws IOException {
+		words = new ResourceWords(input);
+	}
+
+	protected void setWords(URL url) throws IOException {
+		setWords(url.openStream());
+	}
+	
 	protected String getSuccessMessageStringFormat() {
 		return "Yes, %s was found!";
 	}
@@ -26,7 +114,7 @@ public abstract class WordsDict {
 	}
 	
 	public DictSearchResult search(String searchKey) {
-		if (words.hasWord(searchKey)) {
+		if (words != null && words.hasWord(searchKey)) {
 			return new DictSearchResult(true, String.format(getSuccessMessageStringFormat(), searchKey), null);
 		} else {
 			return new DictSearchResult(false, String.format(getFailureMessageStringFormat(), searchKey), null);