diff --git a/pydtn/network.py b/pydtn/network.py
index bc2c26b073a24fab99b7172414e7e3e7389222ea..c624da77fc481b2f2a16de722f771cd2dd952863 100644
--- a/pydtn/network.py
+++ b/pydtn/network.py
@@ -1,4 +1,4 @@
-from collections import OrderedDict
+from collections import defaultdict, Iterable, OrderedDict
 from itertools import combinations
 import random
 
@@ -40,7 +40,7 @@ class Network:
         if node_factory is None:
             node_factory = NodeFactory(tick_rate=1, router=routers['direct'])
         self.nodes = [
-            node_factory(env, self, nid)
+            node_factory(self, nid)
             for nid in range(self.trace.nodes)
         ]
         self.links = [
@@ -91,37 +91,46 @@ class Network:
 
 
 def NodeFactory(router, **kwargs):
-    def factory(env, network, nid):
-        return Node(env, network, nid, router=router, **kwargs)
+    def factory(network, nid):
+        return Node(network, nid, router=router, **kwargs)
     return factory
 
 
 class Node(TickProcess):
     ''''''
-    def __init__(self, env, network, nid,
+    def __init__(self, network, nid,
                  buffer_size=None, tick_time=1, router=None):
         ''''''
         super().__init__(tick_time)
-        self.env = env
-
+        self.env = network.env
         self.network = network
         self.id = nid
 
         self.buffer = Buffer(self.env, capacity=buffer_size)
 
-        # bind router as a class method
         if router is None:
             router = routers['direct']
-        self.router = router #router.__get__(self, Node)
-        self.router_state = {}
+        self.router = router(self)
 
-        self.start(env)
+        self.start(self.env)
 
     def route_packets(self):
         packets_to_delete = []
 
         for packet in self.buffer:
-            if self.router(self, packet, self.router_state):
+            targets, reason, delete = self.router(packet)
+
+            # check if there are targets to send to
+            if targets is None:
+                continue
+
+            # allow for targets to be iterable or single item
+            if not isinstance(targets, Iterable):
+                targets = [targets]
+
+            self.send(targets, packet, reason)
+
+            if delete:
                 packets_to_delete.append(packet)
 
         for packet in packets_to_delete:
@@ -134,10 +143,11 @@ class Node(TickProcess):
             self.route_packets()
             yield self.tick()
 
-    def send(self, to, packet, reason=None):
+    def send(self, targets, packet, reason):
         # TODO: transfer delay
-        packet.send(self, to, reason=reason)
-        self.network.send_link(self, to, packet)
+        packet.send(self, targets, reason=reason)
+        for target in targets:
+            self.network.send_link(self, target, packet)
 
     def recv(self, packet):
         if packet.destination == self:
@@ -269,18 +279,20 @@ class Packet:
 
         self.path = []
 
-        self.stats = dict()
+        self.stats = defaultdict(int)
 
         self.sent = 0
         self.recieved = False
         self.duplicates = 0
         self.dropped = 0
 
-    def send(self, a, b, reason=None):
+    def send(self, sender, targets, reason=None):
+        target_ids = [ n.id for n in targets ]
         if reason is None:
-            self.path.append((a.id, b.id))
+            self.path.append((sender.id, target_ids))
         else:
-            self.path.append((a.id, b.id, reason))
+            self.stats[f'send_{reason}'] += 1
+            self.path.append((sender.id, target_ids, reason))
         self.sent += 1
 
     def recv(self):
diff --git a/pydtn/routers/__init__.py b/pydtn/routers/__init__.py
index 04c66c2b27fb883a47a18c5d866ec4ef6acb1714..ce03567280a5f31d42480a65b4f8cc6be6071de0 100644
--- a/pydtn/routers/__init__.py
+++ b/pydtn/routers/__init__.py
@@ -1,14 +1,14 @@
-from .bubble import bubble
-from .direct import direct
-from .epidemic import epidemic
-from .flooding import flooding
-from .hcbf import hcbf
+from .bubble import BubbleRouter
+from .direct import DirectRouter
+from .epidemic import EpidemicRouter
+from .flooding import FloodingRouter
+from .hcbf import HCBFRouter
 
 types = {
-    'bubble': bubble,
-    'direct': direct,
-    'epidemic': epidemic,
-    'flooding': flooding,
-    'hcbf': hcbf,
+    'bubble': BubbleRouter,
+    'direct': DirectRouter,
+    'epidemic': EpidemicRouter,
+    'flooding': FloodingRouter,
+    'hcbf': HCBFRouter,
 }
 
diff --git a/pydtn/routers/base.py b/pydtn/routers/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fb3fc51fc0d926ff79ee6b502563856d9dd1827
--- /dev/null
+++ b/pydtn/routers/base.py
@@ -0,0 +1,15 @@
+from collections import namedtuple
+
+class Router:
+    '''
+    Base Router class
+    -----------------
+    All routers are callable objects
+    '''
+    def __init__(self, node, **kwargs):
+        self.env = node.env
+        self.node = node
+
+    def __call__(self, packet):
+        raise NotImplementedError
+
diff --git a/pydtn/routers/bubble.py b/pydtn/routers/bubble.py
index 2cc740bf264431497de28734b1f44dfbeabea40f..23afadff1d7ee6e3e17f5336b8e6eb8295cd3be4 100644
--- a/pydtn/routers/bubble.py
+++ b/pydtn/routers/bubble.py
@@ -1,46 +1,19 @@
-def bubble(self, packet, state):
-    ''''''
-    if 'bubble_lp' not in packet.stats:
-        packet.stats = {
-            f'bubble_{reason}': 0
-            for reason in ['direct', 'lp', 'gp']
-        }
+from .community import CommunityRouter
 
+class BubbleRouter(CommunityRouter):
+    def __call__(self, packet):
+        if packet.destination in self.node.links:
+            return packet.destination, 'direct', True
 
-    stats = packet.stats
-    dest = packet.destination
-    community = self.network.community
+        if self.local_community:
+            best, lp = self.best_lp()
+            if lp > self.lp():
+                return best, 'lp', True
 
-    def send(to, reason):
-        stats[f'bubble_{reason}'] += 1
-        self.send(to, packet, reason=reason)
+        elif self.not_local_community:
+            best, gp = self.best_gp()
+            if gp > self.gp():
+                return best, 'gp', True
 
-    if dest in self.links:
-        send(dest, 'direct')
-        return True
-
-    lp = community.get_lp
-    gp = community.get_gp
-
-    local_community = [
-        met for met in self.links if met in self.community
-    ]
-
-    not_local_community = [
-        met for met in self.links if met not in self.community
-    ]
-
-    if local_community:
-        max_lp = max(local_community, key=lp)
-        if lp(max_lp) > lp(self):
-            send(max_lp, 'lp')
-            return True
-
-    elif not_local_community:
-        max_gp = max(not_local_community, key=gp)
-        if gp(max_gp) > gp(self):
-            send(max_gp, 'gp')
-            return True
-
-    return False
+        return None, None, False
 
diff --git a/pydtn/routers/community.py b/pydtn/routers/community.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e343e25bdb54e2777ff718123e8b6bf5f49ed5a
--- /dev/null
+++ b/pydtn/routers/community.py
@@ -0,0 +1,73 @@
+from pydtn.communities import LouvainCommunity
+
+from .base import Router
+
+class CommunityRouter(Router):
+    def __init__(self, node, **kwargs):
+        super().__init__(node, **kwargs)
+
+        self.community = self.node.network.community
+
+    @property
+    def local_community(self):
+        links = self.node.links
+        community = self.node.community
+        return [met for met in links if met in community]
+
+    @property
+    def not_local_community(self):
+        links = self.node.links
+        community = self.node.community
+        return [met for met in links if met not in community]
+
+    def best(self, nodes, key):
+        best = max(nodes, key=key)
+        return best, key(best)
+
+    def ui(self):
+        return self.get_ui(self.node)
+
+    def get_ui(self, x):
+        return self.community.get_ui(x)
+
+    def best_ui(self):
+        return self.best(self.local_community, self.get_ui)
+
+    def lp(self):
+        return self.get_lp(self.node)
+
+    def get_lp(self, x):
+        return self.community.get_lp(x)
+
+    def best_lp(self):
+        return self.best(self.local_community, self.get_lp)
+
+    def gp(self):
+        return self.get_gp(self.node)
+
+    def get_gp(self, x):
+        return self.community.get_gp(x)
+
+    def best_gp(self):
+        return self.best(self.not_local_community, self.get_gp)
+
+    def cbc(self, c_y):
+        return self.get_cbc(self.node, c_y)
+
+    def get_cbc(self, x, c_y):
+        return self.community.get_cbc(x.community, c_y)
+
+    def best_cbc(self, c_y):
+        get_cbc = lambda x: self.get_cbc(x, c_y)
+        return self.best(self.not_local_community, get_cbc)
+
+    def ncf(self, c_y):
+        return self.get_ncf(self.node, c_y)
+
+    def get_ncf(self, x, c_y):
+        return self.community.get_ncf(x, c_y)
+
+    def best_ncf(self, c_y):
+        get_ncf = lambda x: self.get_ncf(x, c_y)
+        return self.best(self.local_community, get_ncf)
+
diff --git a/pydtn/routers/direct.py b/pydtn/routers/direct.py
index 29f2518fe85de9d3b0d2a3bd6e49e13c41e0b9b5..76b314a5de29fef5ac8ca02e4c0f1b0abf4d2933 100644
--- a/pydtn/routers/direct.py
+++ b/pydtn/routers/direct.py
@@ -1,10 +1,8 @@
-def direct(self, packet, state):
-    '''
-    Routes a packet via direct contact to the destination.
-    Returns True if the packet was sent successfully, otherwise False.
-    '''
-    for met in self.links:
-        if packet.destination == met:
-            self.send(met, packet)
-    return False
+from .base import Router
+
+class DirectRouter(Router):
+    def __call__(self, packet):
+        if packet.destination in self.node.links:
+            return packet.destination, 'direct', True
+        return None, None, False
 
diff --git a/pydtn/routers/epidemic.py b/pydtn/routers/epidemic.py
index 1047bb717043012e92dd32d6218ac2ee411d71ad..5605d7e7f60721e6e3cf2a3f0f9abfa38ee0fb6f 100644
--- a/pydtn/routers/epidemic.py
+++ b/pydtn/routers/epidemic.py
@@ -1,17 +1,29 @@
 from collections import defaultdict
 
-def epidemic(self, packet, state):
-    '''
-    Routes a packet if it hasn't sent the packet on a link yet.
-    Always returns False.
-    '''
-    if 'sent' not in state:
-        state['sent'] = defaultdict(dict)
-    sent = state['sent']
+from .base import Router
 
-    for met in self.links:
-        if packet not in sent[met]:
-            sent[met][packet] = True
-            self.send(met, packet)
-    return False
+class EpidemicRouter(Router):
+    def __init__(self, node, **kwargs):
+        super().__init__(node, **kwargs)
+
+        # maps a packet to a set of nodes that the packet has been sent to
+        self.sent = defaultdict(set)
+
+    def __call__(self, packet):
+        # get list of currently encountered nodes that do not have the packet
+        targets = [
+            met
+            for met in self.node.links
+            if met not in self.sent[packet]
+        ]
+
+        # update set of nodes a packet was sent to
+        self.sent[packet].update(targets)
+
+        # return list of nodes to send packet to
+        # (targets, remove from buffer, reason)
+        if targets:
+            return targets, 'epidemic', False
+        else:
+            return None, None, False
 
diff --git a/pydtn/routers/flooding.py b/pydtn/routers/flooding.py
index 6b9ddede1626865184f1a0bf13dbb918eaaf318e..5ddf6ce56a80d27b0622fe9a75425a6a7ecb46c1 100644
--- a/pydtn/routers/flooding.py
+++ b/pydtn/routers/flooding.py
@@ -1,9 +1,6 @@
-def flooding(self, packet, state):
-    '''
-    Routes a packet via flooding, i.e. sends all packets on all links.
-    Always returns False.
-    '''
-    for met in self.links:
-        self.send(met, packet)
-    return False
+from .base import Router
+
+class FloodingRouter(Router):
+    def __call__(self, packet):
+        return self.node.links, 'flood', False
 
diff --git a/pydtn/routers/hcbf.py b/pydtn/routers/hcbf.py
index c8af6dc99cfa11385f6ca96f8c8bb30e2f85d6d1..442ee51473dae8f2ea8f930299d1d2f36a4d07dc 100644
--- a/pydtn/routers/hcbf.py
+++ b/pydtn/routers/hcbf.py
@@ -1,89 +1,35 @@
-def hcbf(self, packet, state):
-    if 'hcbf_ui' not in packet.stats:
-        packet.stats = {
-            f'hcbf_{reason}': 0
-            for reason in ['direct', 'ui', 'lp','ui_lonely', 'lp_lonely',
-                           'cbc', 'ncf' ]
-        }
-
-    stats = packet.stats
-    dest = packet.destination
-    community = self.network.community
-
-    def send(to, reason):
-        stats[f'hcbf_{reason}'] += 1
-        self.send(to, packet, reason=reason)
-
-    # case 1: direct delivery
-    if dest in self.links:
-        send(dest, 'direct')
-        return True
-
-    ui = community.get_ui
-    lp = community.get_lp
-    cbc = lambda n: community.get_cbc(n.community, dest.community)
-    ncf = lambda n: community.get_ncf(n, dest.community)
-
-    local_community = [
-        met for met in self.links if met in self.community
-    ]
-
-    not_local_community = [
-        met for met in self.links if met not in self.community
-    ]
-
-    if self.community is dest.community and local_community:
-        max_ui = max(local_community, key=ui)
-        if ui(max_ui) > ui(self):
-            send(max_ui, 'ui')
-            return True
-        elif ui(max_ui) < ui(self):
-            return False
-        # ui(max_ui) == ui(self)
-
-        max_lp = max(local_community, key=lp)
-        if lp(max_lp) > lp(self):
-            send(max_lp, 'lp')
-            return True
-        elif lp(max_lp) < lp(self):
-            return False
-        # lp(max_lp) == lp(self)
-
-        return False
-
-    if not_local_community:
-        max_cbc = max(not_local_community, key=cbc)
-        if cbc(max_cbc) > cbc(self):
-            send(max_cbc, 'cbc')
-            return True
-        elif cbc(max_cbc) < cbc(self):
-            return False
-        # cbc(max_cbc) == cbc(self)
-
-    if local_community:
-        max_ncf = max(local_community, key=ncf)
-        if ncf(max_ncf) > ncf(self):
-            send(max_ncf, 'ncf')
-            return True
-        elif ncf(max_ncf) < ncf(self):
-            return False
-        # ncf(max_ncf) == ncf(self)
-
-        max_ui = max(local_community, key=ui)
-        if ui(max_ui) > ui(self):
-            send(max_ui, 'ui_lonely')
-            return True
-        elif ui(max_ui) < ui(self):
-            return False
-        # ui(max_ui) == ui(self)
-
-        max_lp = max(local_community, key=lp)
-        if lp(max_lp) > lp(self):
-            send(max_lp, 'lp_lonely')
-            return True
-        elif lp(max_lp) < lp(self):
-            return False
-        # lp(max_lp) == lp(self)
-
-    return False
+from .community import CommunityRouter
+
+class HCBFRouter(CommunityRouter):
+    def __call__(self, packet):
+        me = self.node
+        dest = packet.destination
+
+        if dest in me.links:
+            return dest, 'direct', True
+
+        if me.community is not dest.community and self.not_local_community:
+            best, cbc = self.best_cbc(dest.community)
+            if cbc > self.cbc(dest.community):
+                return best, 'cbc', True
+
+        if self.local_community:
+            if me.community is not dest.community:
+                best, ncf = self.best_ncf(dest.community)
+                if ncf > self.ncf(dest.community):
+                    return best, 'ncf', True
+                elif ncf < self.ncf(dest.community):
+                    return None, None, False
+
+            best, ui = self.best_ui()
+            if ui > self.ui():
+                return best, 'ui', True
+            elif ui < self.ui():
+                return None, None, False
+
+            best, lp = self.best_lp()
+            if lp > self.lp():
+                return best, 'lp', True
+
+        return None, None, False