diff --git a/ota/raucadapter/raucadapter.go b/ota/raucadapter/raucadapter.go
index 34b4b2070bfa891fba270f5d8f6392723777307b..6e5e1f6d51769ee600aec73bec3ab0f0a526b8cf 100644
--- a/ota/raucadapter/raucadapter.go
+++ b/ota/raucadapter/raucadapter.go
@@ -10,6 +10,7 @@ import (
 	"errors"
 
 	"booting.oniroproject.org/distro/components/sysota/boot"
+	"booting.oniroproject.org/distro/components/sysota/ota"
 	"booting.oniroproject.org/distro/components/sysota/rauc/installhandler"
 )
 
@@ -22,17 +23,8 @@ var ErrProtocolViolation = errors.New("violation of protocol between SystemOTA a
 
 // Adapter uses SystemOTA boot.Protocol and ota.SystemState to implement boothandler.Handler.
 type Adapter struct {
-	proto          boot.Protocol
-	bootModeHolder BootModeHolder
-}
-
-// BootModeHolder is an interface exposing boot mode control.
-//
-// This interface exits, mainly, so that modifications of BootMode can be
-// broadcast over D-Bus.
-type BootModeHolder interface {
-	BootMode() boot.Mode
-	SetBootMode(boot.Mode) error
+	proto boot.Protocol
+	ps    *ota.StateGuard
 }
 
 // New returns a new Adapter implemented wrapping given boot.Protocol.
@@ -43,8 +35,8 @@ type BootModeHolder interface {
 // interface and has only one element of state that is not captured in the boot
 // loader itself, namely boot.Mode, where the system may be expected to normally
 // or in the one-off try mode.
-func New(proto boot.Protocol, bootModeHolder BootModeHolder) *Adapter {
-	return &Adapter{proto: proto, bootModeHolder: bootModeHolder}
+func New(proto boot.Protocol, ps *ota.StateGuard) *Adapter {
+	return &Adapter{proto: proto, ps: ps}
 }
 
 // PrimarySlot returns the name of the RAUC primary boot slot.
@@ -53,7 +45,7 @@ func New(proto boot.Protocol, bootModeHolder BootModeHolder) *Adapter {
 // state is used to pick either active slot (in normal mode) or inactive slot
 // (in try mode).
 func (adapter *Adapter) PrimarySlot() (boot.Slot, error) {
-	switch adapter.bootModeHolder.BootMode() {
+	switch adapter.bootMode() {
 	case boot.Normal:
 		return adapter.queryActiveOrPristine()
 	case boot.Try:
@@ -85,7 +77,7 @@ func (adapter *Adapter) SetPrimarySlot(slot boot.Slot) error {
 		return err
 	}
 
-	switch adapter.bootModeHolder.BootMode() {
+	switch adapter.bootMode() {
 	case boot.Normal:
 		if slot == activeSlot {
 			// No-op, in normal mode, the active slot is always primary.
@@ -100,7 +92,7 @@ func (adapter *Adapter) SetPrimarySlot(slot boot.Slot) error {
 			return err
 		}
 
-		return adapter.bootModeHolder.SetBootMode(boot.Try)
+		return adapter.setBootMode(boot.Try)
 	case boot.Try:
 		if slot == activeSlot {
 			// Protocol violation, this does not happen during "rauc install".
@@ -182,13 +174,12 @@ func (adapter *Adapter) SetSlotState(slot boot.Slot, state boot.SlotState) error
 	}
 
 	inactive := boot.SynthesizeInactiveSlot(active)
-	bootMode := adapter.bootModeHolder.BootMode()
 
 	// Switch by boot mode, then switch by slot state and finally look at the
 	// slot we've got. This exact same logic can be re-written in several
 	// different styles but I've found this to be easiest to grasp while
 	// retaining consistency.
-	switch bootMode {
+	switch adapter.bootMode() {
 	case boot.Normal:
 		switch state {
 		case boot.GoodSlot:
@@ -227,13 +218,13 @@ func (adapter *Adapter) SetSlotState(slot boot.Slot, state boot.SlotState) error
 				return err
 			}
 
-			return adapter.bootModeHolder.SetBootMode(boot.Normal)
+			return adapter.setBootMode(boot.Normal)
 		case boot.BadSlot:
 			if slot == inactive {
 				// Rollback of an update transaction.
 				// Note that we set boot mode even if CancelSwitch failed because by
 				// this time, we have rolled back already.
-				if err := adapter.bootModeHolder.SetBootMode(boot.Normal); err != nil {
+				if err := adapter.setBootMode(boot.Normal); err != nil {
 					return err
 				}
 
@@ -294,3 +285,18 @@ func (adapter *Adapter) queryInactiveOrPristine() (boot.Slot, error) {
 
 	return active, err
 }
+
+func (adapter *Adapter) bootMode() boot.Mode {
+	return adapter.ps.State().BootMode
+}
+
+func (adapter *Adapter) setBootMode(bootMode boot.Mode) error {
+	return adapter.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		if st.BootMode == bootMode {
+			return false, nil
+		}
+
+		st.BootMode = bootMode
+		return true, nil
+	})
+}
diff --git a/ota/raucadapter/raucadapter_test.go b/ota/raucadapter/raucadapter_test.go
index fe0cf39e929bed6dcde2e049ac3262a74119d3c5..84b2267ee7a6bb32b87e512bee189c31ebb32945 100644
--- a/ota/raucadapter/raucadapter_test.go
+++ b/ota/raucadapter/raucadapter_test.go
@@ -11,6 +11,7 @@ import (
 
 	"booting.oniroproject.org/distro/components/sysota/boot"
 	"booting.oniroproject.org/distro/components/sysota/boot/testboot"
+	"booting.oniroproject.org/distro/components/sysota/ota"
 	"booting.oniroproject.org/distro/components/sysota/ota/raucadapter"
 	"booting.oniroproject.org/distro/components/sysota/rauc/installhandler"
 	"booting.oniroproject.org/distro/components/sysota/rauc/rauctest"
@@ -21,10 +22,10 @@ func Test(t *testing.T) { TestingT(t) }
 var errBoom = errors.New("boom")
 
 type adapterSuite struct {
-	proto     testboot.Protocol
-	bootState rauctest.BootState
-	adapter   *raucadapter.Adapter
-	called    int
+	proto   testboot.Protocol
+	ps      ota.StateGuard
+	adapter *raucadapter.Adapter
+	called  int
 
 	// variant holds the description of the test suite variant.
 	variant CommentInterface
@@ -38,19 +39,22 @@ type adapterSuite struct {
 }
 
 func (s *adapterSuite) SetUpSuite(c *C) {
-	s.adapter = raucadapter.New(&s.proto, &s.bootState)
+	s.adapter = raucadapter.New(&s.proto, &s.ps)
 }
 
 func (s *adapterSuite) SetUpTest(c *C) {
 	s.cleanSlate(c)
 	// This is only set in the per-test setup so that individual tests
 	// can call cleanSlate and still observe the changes to the boot mode.
-	c.Assert(s.bootState.SetBootMode(boot.Normal), IsNil)
+	err := s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Normal
+		return true, nil
+	})
+	c.Assert(err, IsNil)
 }
 
 func (s *adapterSuite) cleanSlate(c *C) {
 	s.proto.ResetCallbacks()
-	s.bootState.BootModeChanged = nil
 	s.called = 0
 }
 
@@ -97,7 +101,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) {
 	err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot)
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 
 	// custom: set-primary $toSlot
 	s.cleanSlate(c)
@@ -124,7 +128,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) {
 	err = s.adapter.SetPrimarySlot(s.toSlot)
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 2, s.variant)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// TODO(zyga): Conceptually, we would reboot with the special flag here.
 	// This is not tested yet, as it requires cooperation abetween RAUC
@@ -147,7 +151,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, s.toSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// custom: get-state $toSlot
 	s.cleanSlate(c)
@@ -165,7 +169,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(state, Equals, boot.BadSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// custom: get-state $fromSlot
 	s.cleanSlate(c)
@@ -183,7 +187,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(state, Equals, boot.GoodSlot, s.variant)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// custom: set-state $toSlot good
 	s.cleanSlate(c)
@@ -206,7 +210,7 @@ func (s *adapterSuite) TestInstallGoodPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 2)
 	// Note that boot.Mode switches to Normal.
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
@@ -230,7 +234,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
 	err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot)
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 
 	// custom: set-primary $toSlot
 	s.cleanSlate(c)
@@ -256,7 +260,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
 	err = s.adapter.SetPrimarySlot(s.toSlot)
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 2, s.variant)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// TODO(zyga): Conceptually, we would reboot with the special flag here.
 	// This is not tested yet, as it requires cooperation abetween RAUC
@@ -278,7 +282,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, s.toSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// custom: get-state $toSlot
 	s.cleanSlate(c)
@@ -296,7 +300,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(state, Equals, boot.BadSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// custom: get-state $fromSlot
 	s.cleanSlate(c)
@@ -314,7 +318,7 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(state, Equals, boot.GoodSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 
 	// custom: set-state $toSlot bad
 	s.cleanSlate(c)
@@ -337,13 +341,16 @@ func (s *adapterSuite) TestInstallBadPlaythrough(c *C) {
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 2)
 	// Note that boot.Mode switches to Normal.
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 // Tests for BootProtocolAdapter.PrimarySlot.
 
 func (s *adapterSuite) TestPrimarySlotInNormalMode(c *C) {
-	c.Assert(s.bootState.SetBootMode(boot.Normal), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Normal
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 
@@ -357,11 +364,14 @@ func (s *adapterSuite) TestPrimarySlotInNormalMode(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, s.fromSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestPrimarySlotInTryMode(c *C) {
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryInactiveFn = func() (boot.Slot, error) {
 		s.called++
 
@@ -375,11 +385,14 @@ func (s *adapterSuite) TestPrimarySlotInTryMode(c *C) {
 	c.Assert(err, IsNil, s.variant)
 	c.Check(slot, Equals, s.toSlot)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 }
 
 func (s *adapterSuite) TestPrimarySlotInInvalidMode(c *C) {
-	c.Assert(s.bootState.SetBootMode(boot.InvalidBootMode), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.InvalidBootMode
+		return true, nil
+	}), IsNil)
 	_, err := s.adapter.PrimarySlot()
 	c.Assert(err, ErrorMatches, `invalid boot mode`)
 }
@@ -387,7 +400,10 @@ func (s *adapterSuite) TestPrimarySlotInInvalidMode(c *C) {
 // Tests for BootProtocolAdapter.SetPrimarySlot.
 
 func (s *adapterSuite) TestSetPrimarySlotToActiveInNormalMode(c *C) {
-	c.Assert(s.bootState.SetBootMode(boot.Normal), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Normal
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 
@@ -400,7 +416,7 @@ func (s *adapterSuite) TestSetPrimarySlotToActiveInNormalMode(c *C) {
 	err := s.adapter.SetPrimarySlot(s.fromSlot)
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestSetPrimaryToInactiveInNormalMode(c *C) {
@@ -418,11 +434,14 @@ func (s *adapterSuite) TestSetPrimaryToInactiveInNormalMode(c *C) {
 	}
 	err := s.adapter.SetPrimarySlot(s.toSlot)
 	c.Assert(err, IsNil)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try, s.variant)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try, s.variant)
 }
 
 func (s *adapterSuite) TestSetPrimarySlotToActiveInTryMode(c *C) {
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		if s.pristine {
 			return boot.InvalidSlot, boot.ErrPristineBootConfig
@@ -443,7 +462,10 @@ func (s *adapterSuite) TestSetPrimarySlotToInactiveInTryMode(c *C) {
 	activeSlot := s.fromSlot
 	inactiveSlot := boot.SynthesizeInactiveSlot(activeSlot)
 
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 
@@ -456,7 +478,7 @@ func (s *adapterSuite) TestSetPrimarySlotToInactiveInTryMode(c *C) {
 	err := s.adapter.SetPrimarySlot(inactiveSlot)
 	c.Assert(err, IsNil, s.variant)
 	c.Assert(s.called, Equals, 1)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 }
 
 func (s *adapterSuite) TestSetPrimarySlotToPrimarySlot(c *C) {
@@ -478,15 +500,17 @@ func (s *adapterSuite) TestSetPrimarySlotToPrimarySlot(c *C) {
 
 	for _, bootMode := range []boot.Mode{boot.Normal, boot.Try} {
 		comment := Commentf("%s, boot mode: %s", s.variant.CheckCommentString(), bootMode)
-		err := s.bootState.SetBootMode(bootMode)
-		c.Assert(err, IsNil, comment)
+		c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+			st.BootMode = bootMode
+			return true, nil
+		}), IsNil, comment)
 
 		primarySlot, err := s.adapter.PrimarySlot()
 		c.Assert(err, IsNil, comment)
 		err = s.adapter.SetPrimarySlot(primarySlot)
 		c.Assert(err, IsNil, comment)
 
-		c.Check(s.bootState.BootMode(), Equals, bootMode, comment)
+		c.Check(s.ps.State().BootMode, Equals, bootMode, comment)
 	}
 }
 
@@ -519,12 +543,14 @@ func (s *adapterSuite) TestSetPrimaryToInactiveButTrySwitchFailed(c *C) {
 	c.Assert(err, ErrorMatches, `boom`, s.variant)
 
 	// Note that a transaction was not started.
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestSetPrimaryButBootModeIsInvalid(c *C) {
-	err := s.bootState.SetBootMode(boot.InvalidBootMode)
-	c.Check(err, IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.InvalidBootMode
+		return true, nil
+	}), IsNil)
 
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		if s.pristine {
@@ -534,7 +560,7 @@ func (s *adapterSuite) TestSetPrimaryButBootModeIsInvalid(c *C) {
 		return s.fromSlot, nil
 	}
 
-	err = s.adapter.SetPrimarySlot(s.fromSlot)
+	err := s.adapter.SetPrimarySlot(s.fromSlot)
 	c.Assert(err, ErrorMatches, `invalid boot mode`, s.variant)
 }
 
@@ -645,7 +671,10 @@ func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToBad(c *C) {
 	// that rauc reads and writes the state and any combination can be
 	// expressed. SystemOTA synthesizes the state that RAUC expects based on the
 	// boot.Mode alone (that is, one bit of information).
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		if s.pristine {
 			return boot.InvalidSlot, boot.ErrPristineBootConfig
@@ -658,7 +687,10 @@ func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToBad(c *C) {
 }
 
 func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToGood(c *C) {
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		if s.pristine {
 			return boot.InvalidSlot, boot.ErrPristineBootConfig
@@ -673,7 +705,10 @@ func (s *adapterSuite) TestSetSlotStateOfActiveSlotInTryModeToGood(c *C) {
 func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBad(c *C) {
 	// While in try-boot mode, setting the state of the inactive slot
 	// to bad cancels the update transaction.
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 		if s.pristine {
@@ -692,13 +727,16 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBad(c *C) {
 	err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot)
 	c.Assert(err, IsNil, s.variant)
 	c.Assert(s.called, Equals, 2)
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGood(c *C) {
 	// While in try-boot mode, setting the state of the inactive slot
 	// to good commits the update transaction.
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 		// Note that $fromSlot is active.
@@ -713,7 +751,7 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGood(c *C) {
 	err := s.adapter.SetSlotState(s.toSlot, boot.GoodSlot)
 	c.Assert(err, IsNil)
 	c.Assert(s.called, Equals, 2)
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestSetSlotStateOfInvalidSlot(c *C) {
@@ -737,7 +775,10 @@ func (s *adapterSuite) TestSetSlotStateButQueryActiveFailed(c *C) {
 
 func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButSetBootModeFailed(c *C) {
 	// We are trying to cancel the transaction but we failed to set the new boot mode.
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 		if s.pristine {
@@ -746,21 +787,26 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButSetBootMod
 
 		return s.fromSlot, nil
 	}
-	s.bootState.BootModeChanged = func(bootMode boot.Mode) error {
-		c.Assert(bootMode, Equals, boot.Normal)
+	s.ps.RegisterObserver("test", func(st *ota.SystemState) error {
+		c.Assert(st.BootMode, Equals, boot.Normal)
 		s.called++
 
 		return errBoom
-	}
+	})
+	defer s.ps.RemoveObserver("test")
+
 	err := s.adapter.SetSlotState(s.toSlot, boot.BadSlot)
 	c.Assert(err, ErrorMatches, `boom`, s.variant)
 	c.Assert(s.called, Equals, 2)
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 }
 
 func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButCancelSwitchFailed(c *C) {
 	// We are trying to cancel the transaction but that operation fails.
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 
@@ -786,13 +832,16 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToBadButCancelSwit
 	//
 	// CancelSwitch exists only to (perhaps) remove temporary files that were
 	// required for the duration of TrySwitch lifetime.
-	c.Check(s.bootState.BootMode(), Equals, boot.Normal)
+	c.Check(s.ps.State().BootMode, Equals, boot.Normal)
 }
 
 func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGoodButCommitSwitchFailed(c *C) {
 	// While in try-boot mode, setting the state of the inactive slot
 	// to good commits the update transaction.
-	c.Assert(s.bootState.SetBootMode(boot.Try), IsNil)
+	c.Assert(s.ps.AlterState(func(st *ota.SystemState) (bool, error) {
+		st.BootMode = boot.Try
+		return true, nil
+	}), IsNil)
 	s.proto.QueryActiveFn = func() (boot.Slot, error) {
 		s.called++
 
@@ -821,7 +870,7 @@ func (s *adapterSuite) TestSetSlotStateOfInactiveSlotInTryModeToGoodButCommitSwi
 	// reboot.
 	//
 	// XXX(zyga): This may warrant a special error type to handle properly.
-	c.Check(s.bootState.BootMode(), Equals, boot.Try)
+	c.Check(s.ps.State().BootMode, Equals, boot.Try)
 }
 
 func (s *adapterSuite) TestPreInstallHandlerWithUnawareBootProtocol(c *C) {
@@ -843,7 +892,7 @@ func (s *adapterSuite) TestPreInstallHandlerWithAwareBootProtocol(c *C) {
 			},
 		},
 	}
-	adapter := raucadapter.New(proto, &s.bootState)
+	adapter := raucadapter.New(proto, &s.ps)
 
 	err := adapter.PreInstallHandler(ctx)
 	c.Assert(err, IsNil)
@@ -864,7 +913,7 @@ func (s *adapterSuite) TestPostInstallHandlerWithUnawareBootProtocol(c *C) {
 			},
 		},
 	}
-	adapter := raucadapter.New(proto, &s.bootState)
+	adapter := raucadapter.New(proto, &s.ps)
 
 	err := adapter.PostInstallHandler(ctx)
 	c.Assert(err, IsNil)
diff --git a/ota/raucinterface/service.go b/ota/raucinterface/service.go
index a86c292a2a5379dd9c71c8b74b1758f7bce9d8a5..25a9404f02fe6648eccf2e78c9fe394640e09f59 100644
--- a/ota/raucinterface/service.go
+++ b/ota/raucinterface/service.go
@@ -34,8 +34,8 @@ const (
 	contextArgName = "context"
 )
 
-// Service implements dbusutil.HostedService exposing boothandler.Handler and installhandler.Participant over D-Bus.
-type Service struct {
+// HostedService implements dbusutil.HostedService exposing boothandler.Handler and installhandler.Participant over D-Bus.
+type HostedService struct {
 	bootHandler    boothandler.Handler
 	installHandler installhandler.Participant
 
@@ -43,12 +43,12 @@ type Service struct {
 }
 
 // NewService returns a service with the given boot backend.
-func NewService(bootHandler boothandler.Handler, installHandler installhandler.Participant) *Service {
-	return &Service{bootHandler: bootHandler, installHandler: installHandler}
+func NewService(bootHandler boothandler.Handler, installHandler installhandler.Participant) *HostedService {
+	return &HostedService{bootHandler: bootHandler, installHandler: installHandler}
 }
 
 // JoinServiceHost integrates with dbusutil.ServiceHost.
-func (svc *Service) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
+func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
 	svc.conn = reg.DBusConn()
 
 	methods := map[string]interface{}{
@@ -143,7 +143,7 @@ func (svc *Service) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
 }
 
 // GetPrimary maps boothandler.Handler.PrimarySlot to D-Bus.
-func (svc *Service) GetPrimary() (slotName string, dbusErr *dbus.Error) {
+func (svc *HostedService) GetPrimary() (slotName string, dbusErr *dbus.Error) {
 	slot, err := svc.bootHandler.PrimarySlot()
 	if err != nil {
 		return "", dbus.MakeFailedError(err)
@@ -153,7 +153,7 @@ func (svc *Service) GetPrimary() (slotName string, dbusErr *dbus.Error) {
 }
 
 // SetPrimary maps boothandler.Handler.SetPrimarySlot to D-Bus.
-func (svc *Service) SetPrimary(slotName string) (dbusErr *dbus.Error) {
+func (svc *HostedService) SetPrimary(slotName string) (dbusErr *dbus.Error) {
 	var slot boot.Slot
 
 	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
@@ -168,7 +168,7 @@ func (svc *Service) SetPrimary(slotName string) (dbusErr *dbus.Error) {
 }
 
 // GetState maps boothandler.Handler.SlotState to D-Bus.
-func (svc *Service) GetState(slotName string) (bootState string, dbusErr *dbus.Error) {
+func (svc *HostedService) GetState(slotName string) (bootState string, dbusErr *dbus.Error) {
 	var slot boot.Slot
 
 	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
@@ -184,7 +184,7 @@ func (svc *Service) GetState(slotName string) (bootState string, dbusErr *dbus.E
 }
 
 // SetState maps boothandler.Handler.SetSlotState to D-Bus.
-func (svc *Service) SetState(slotName string, stateName string) (dbusErr *dbus.Error) {
+func (svc *HostedService) SetState(slotName string, stateName string) (dbusErr *dbus.Error) {
 	var slot boot.Slot
 
 	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
@@ -204,7 +204,7 @@ func (svc *Service) SetState(slotName string, stateName string) (dbusErr *dbus.E
 	return nil
 }
 
-func (svc *Service) populateHandlerPID(ctx *installhandler.HandlerContext, sender dbus.Sender) error {
+func (svc *HostedService) populateHandlerPID(ctx *installhandler.HandlerContext, sender dbus.Sender) error {
 	// Find the pid of the sender and store it int the handler context.
 	// See https://dbus.freedesktop.org/doc/dbus-specification.html
 	var pid uint32
@@ -219,7 +219,7 @@ func (svc *Service) populateHandlerPID(ctx *installhandler.HandlerContext, sende
 }
 
 // PreInstall maps installhandler.Participant.PreInstallHandler to D-Bus.
-func (svc *Service) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) {
+func (svc *HostedService) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) {
 	ctx, err := installhandler.NewHandlerContext(env)
 	if err != nil {
 		return dbus.MakeFailedError(err)
@@ -237,7 +237,7 @@ func (svc *Service) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.
 }
 
 // PostInstall maps installhandler.Participant.PostInstallHandler to D-Bus.
-func (svc *Service) PostInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) {
+func (svc *HostedService) PostInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) {
 	ctx, err := installhandler.NewHandlerContext(env)
 	if err != nil {
 		return dbus.MakeFailedError(err)
diff --git a/rauc/rauctest/boothandler.go b/rauc/rauctest/boothandler.go
index ac6c173b1cd79961650080c97a7156928c4015eb..8b6ca11f4317cc20797ad41deabf6ab957f6d768 100644
--- a/rauc/rauctest/boothandler.go
+++ b/rauc/rauctest/boothandler.go
@@ -57,36 +57,3 @@ func (handler *BootHandler) SetSlotState(slot boot.Slot, state boot.SlotState) e
 
 	return handler.SetSlotStateFn(slot, state)
 }
-
-// BootState implements rauc.BootModeHolder for the BootProtocolAdapter
-type BootState struct {
-	bootMode boot.Mode
-
-	// BootModeChanged can be used to observe or verify changes to boot mode.
-	BootModeChanged func(boot.Mode) error
-}
-
-// BootMode returns the current boot mode.
-func (state *BootState) BootMode() boot.Mode {
-	return state.bootMode
-}
-
-// SetBootMode sets the new boot mode.
-//
-// Changes to boot mode are propagated to BootModeChanged callback before
-// becoming effective, enabling both notifications and validation use-cases.
-func (state *BootState) SetBootMode(bootMode boot.Mode) error {
-	if state.bootMode == bootMode {
-		return nil
-	}
-
-	if state.BootModeChanged != nil {
-		if err := state.BootModeChanged(bootMode); err != nil {
-			return err
-		}
-	}
-
-	state.bootMode = bootMode
-
-	return nil
-}
diff --git a/rauc/rauctest/boothandler_test.go b/rauc/rauctest/boothandler_test.go
index d1a49d0b36c19090d49a906c7e971e3db0de6065..609ee552b885697b39d43b812e0bacf8911c4632 100644
--- a/rauc/rauctest/boothandler_test.go
+++ b/rauc/rauctest/boothandler_test.go
@@ -102,54 +102,3 @@ func (s *bootBackendSuite) TestResetCallbacks(c *C) {
 
 	c.Check(backend, DeepEquals, rauctest.BootHandler{})
 }
-
-type bootStateSuite struct{}
-
-var _ = Suite(&bootStateSuite{})
-
-func (s *bootStateSuite) TestBootModeReadWrite(c *C) {
-	bootState := rauctest.BootState{}
-
-	c.Check(bootState.BootMode(), Equals, boot.Normal)
-
-	err := bootState.SetBootMode(boot.Try)
-	c.Assert(err, IsNil)
-	c.Check(bootState.BootMode(), Equals, boot.Try)
-}
-
-func (s *bootStateSuite) TestBootModeChangeObserver(c *C) {
-	changes := make([]boot.Mode, 0, 3)
-	bootState := rauctest.BootState{
-		BootModeChanged: func(newBootMode boot.Mode) error {
-			changes = append(changes, newBootMode)
-			return nil
-		},
-	}
-
-	c.Assert(bootState.SetBootMode(boot.Normal), IsNil)
-	c.Check(bootState.BootMode(), Equals, boot.Normal)
-	c.Assert(bootState.SetBootMode(boot.Try), IsNil)
-	c.Check(bootState.BootMode(), Equals, boot.Try)
-	c.Assert(bootState.SetBootMode(boot.Normal), IsNil)
-	c.Check(bootState.BootMode(), Equals, boot.Normal)
-
-	c.Check(changes, DeepEquals, []boot.Mode{boot.Try, boot.Normal})
-}
-
-func (s *bootStateSuite) TestBootModeChangeGuard(c *C) {
-	bootState := rauctest.BootState{
-		BootModeChanged: func(newBootMode boot.Mode) error {
-			if newBootMode != boot.Normal && newBootMode != boot.Try {
-				return boot.ErrInvalidBootMode
-			}
-			return nil
-		},
-	}
-
-	c.Assert(bootState.SetBootMode(boot.Normal), IsNil)
-	c.Check(bootState.BootMode(), Equals, boot.Normal)
-	c.Assert(bootState.SetBootMode(boot.Try), IsNil)
-	c.Check(bootState.BootMode(), Equals, boot.Try)
-	c.Assert(bootState.SetBootMode(boot.InvalidBootMode), ErrorMatches, `invalid boot mode`)
-	c.Check(bootState.BootMode(), Equals, boot.Try)
-}
diff --git a/service/bootprotoservice.go b/service/boothosted/boothosted.go
similarity index 74%
rename from service/bootprotoservice.go
rename to service/boothosted/boothosted.go
index 4ec07588edfe1cd46da576bf8e7ad2c9ec404102..13ebcbcf02c56c92dc43e504efb3844ccf48d690 100644
--- a/service/bootprotoservice.go
+++ b/service/boothosted/boothosted.go
@@ -1,8 +1,9 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-FileCopyrightText: Huawei Inc.
 
-// Package service implements the sysotad D-Bus service.
-package service
+// Package boothosted contains the implementation of hosted service exposing the
+// boot protocol over D-Bus.
+package boothosted
 
 import (
 	"errors"
@@ -16,13 +17,13 @@ import (
 )
 
 const (
-	// BootLoaderInterfaceName is the name of the SystemOTA BootLoader interface.
-	BootLoaderInterfaceName = "org.oniroproject.sysota1.BootLoader"
+	// InterfaceName is the name of the SystemOTA BootLoader interface.
+	InterfaceName = "org.oniroproject.sysota1.BootLoader"
 )
 
-// BootLoaderIntrospectData is the D-Bus introspection data for the BootLoader interface of the service object.
-var BootLoaderIntrospectData = introspect.Interface{
-	Name: BootLoaderInterfaceName,
+// IntrospectData is the D-Bus introspection data for the BootLoader interface of the service object.
+var IntrospectData = introspect.Interface{
+	Name: InterfaceName,
 	Methods: []introspect.Method{
 		{
 			Name: "QueryActive",
@@ -75,13 +76,17 @@ var BootLoaderIntrospectData = introspect.Interface{
 
 var errNoBootProtocol = errors.New("boot protocol is not set")
 
-// BootProtocolService is a dbusutil.HostedService exporting boot.Protocol over D-BUs.
-type BootProtocolService struct {
+// HostedService is a dbusutil.HostedService exporting boot.Protocol over D-BUs.
+type HostedService struct {
 	bootProto boot.Protocol
 }
 
+func (svc *HostedService) Init(p boot.Protocol) {
+	svc.bootProto = p
+}
+
 // JoinServiceHost integrates with dbusutil.ServiceHost.
-func (svc *BootProtocolService) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
+func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
 	methods := map[string]interface{}{
 		"QueryActive":   svc.QueryActive,
 		"QueryInactive": svc.QueryInactive,
@@ -91,11 +96,11 @@ func (svc *BootProtocolService) JoinServiceHost(reg dbusutil.ServiceRegistration
 		"CancelSwitch":  svc.CancelSwitch,
 	}
 
-	return reg.RegisterObject(ota.ServiceObjectPath, BootLoaderInterfaceName, methods, BootLoaderIntrospectData)
+	return reg.RegisterObject(ota.ServiceObjectPath, InterfaceName, methods, IntrospectData)
 }
 
 // QueryActive exposes the boot.Protocol.QueryActive method over D-Bus.
-func (svc *BootProtocolService) QueryActive() (string, *dbus.Error) {
+func (svc *HostedService) QueryActive() (string, *dbus.Error) {
 	if svc.bootProto == nil {
 		return "", dbus.MakeFailedError(errNoBootProtocol)
 	}
@@ -109,7 +114,7 @@ func (svc *BootProtocolService) QueryActive() (string, *dbus.Error) {
 }
 
 // QueryInactive exposes the boot.Protocol.QueryInactive method over D-Bus.
-func (svc *BootProtocolService) QueryInactive() (string, *dbus.Error) {
+func (svc *HostedService) QueryInactive() (string, *dbus.Error) {
 	if svc.bootProto == nil {
 		return "", dbus.MakeFailedError(errNoBootProtocol)
 	}
@@ -123,7 +128,7 @@ func (svc *BootProtocolService) QueryInactive() (string, *dbus.Error) {
 }
 
 // TrySwitch exposes the boot.Protocol.TrySwitch method over D-Bus.
-func (svc *BootProtocolService) TrySwitch(slotName string) *dbus.Error {
+func (svc *HostedService) TrySwitch(slotName string) *dbus.Error {
 	if svc.bootProto == nil {
 		return dbus.MakeFailedError(errNoBootProtocol)
 	}
@@ -143,7 +148,7 @@ func (svc *BootProtocolService) TrySwitch(slotName string) *dbus.Error {
 // Reboot exposes the boot.Protocol.Reboot method over D-Bus.
 //
 // The flags argument is not translated in any way.
-func (svc *BootProtocolService) Reboot(flags uint) *dbus.Error {
+func (svc *HostedService) Reboot(flags uint) *dbus.Error {
 	if svc.bootProto == nil {
 		return dbus.MakeFailedError(errNoBootProtocol)
 	}
@@ -156,7 +161,7 @@ func (svc *BootProtocolService) Reboot(flags uint) *dbus.Error {
 }
 
 // CommitSwitch exposes the boot.Protocol.CommitSwitch method over D-Bus.
-func (svc *BootProtocolService) CommitSwitch() *dbus.Error {
+func (svc *HostedService) CommitSwitch() *dbus.Error {
 	if svc.bootProto == nil {
 		return dbus.MakeFailedError(errNoBootProtocol)
 	}
@@ -169,7 +174,7 @@ func (svc *BootProtocolService) CommitSwitch() *dbus.Error {
 }
 
 // CancelSwitch exposes the boot.Protocol.CancelSwitch method over D-Bus.
-func (svc *BootProtocolService) CancelSwitch() *dbus.Error {
+func (svc *HostedService) CancelSwitch() *dbus.Error {
 	if svc.bootProto == nil {
 		return dbus.MakeFailedError(errNoBootProtocol)
 	}
diff --git a/service/export_test.go b/service/export_test.go
index a56d447a7b259ef648a7e6ce987ddaf79afe6b96..9cdd67f6713a2319f5c1d0a48414e4d03029a427 100644
--- a/service/export_test.go
+++ b/service/export_test.go
@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-FileCopyrightText: Huawei Inc.
+
 package service
 
 import (
diff --git a/service/rauchosted/rauchosted.go b/service/rauchosted/rauchosted.go
new file mode 100644
index 0000000000000000000000000000000000000000..797a0ee090f5c7990b3d79952990e9f893fd0d2e
--- /dev/null
+++ b/service/rauchosted/rauchosted.go
@@ -0,0 +1,260 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Huawei Inc.
+
+package rauchosted
+
+import (
+	"github.com/godbus/dbus/v5"
+	"github.com/godbus/dbus/v5/introspect"
+
+	"booting.oniroproject.org/distro/components/sysota/boot"
+	"booting.oniroproject.org/distro/components/sysota/dbusutil"
+	"booting.oniroproject.org/distro/components/sysota/ota"
+	"booting.oniroproject.org/distro/components/sysota/rauc/boothandler"
+	"booting.oniroproject.org/distro/components/sysota/rauc/installhandler"
+)
+
+const (
+	// InterfaceName is the name of the RAUC interface provided by SystemOTA.
+	InterfaceName = "org.oniroproject.sysota1.RAUC"
+)
+
+const (
+	// Methods of the boot handler
+
+	getPrimaryDBusName = "GetPrimary"
+	setPrimaryDBusName = "SetPrimary"
+	getStateDBusName   = "GetState"
+	setStateDBusName   = "SetState"
+
+	// Methods of the install handler
+
+	preInstallDBusName  = "PreInstall"
+	postInstallDBusName = "PostInstall"
+
+	// Argument names
+
+	slotArgName    = "slot"
+	stateArgName   = "state"
+	contextArgName = "context"
+)
+
+// HostedService implements dbusutil.HostedService exposing boothandler.Handler and installhandler.Participant over D-Bus.
+type HostedService struct {
+	bootHandler    boothandler.Handler
+	installHandler installhandler.Participant
+
+	conn *dbus.Conn
+}
+
+func (svc *HostedService) Init(bootHandler boothandler.Handler, installHandler installhandler.Participant) {
+	svc.bootHandler = bootHandler
+	svc.installHandler = installHandler
+}
+
+// JoinServiceHost integrates with dbusutil.ServiceHost.
+func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
+	svc.conn = reg.DBusConn()
+
+	methods := map[string]interface{}{
+		getPrimaryDBusName: svc.GetPrimary,
+		setPrimaryDBusName: svc.SetPrimary,
+		getStateDBusName:   svc.GetState,
+		setStateDBusName:   svc.SetState,
+
+		preInstallDBusName:  svc.PreInstall,
+		postInstallDBusName: svc.PostInstall,
+	}
+
+	metadata := introspect.Interface{
+		Name: InterfaceName,
+		Methods: []introspect.Method{
+			{
+				Name: getPrimaryDBusName,
+				Args: []introspect.Arg{
+					{
+						Name:      slotArgName,
+						Type:      "s",
+						Direction: dbusutil.Out,
+					},
+				},
+			},
+			{
+				Name: setPrimaryDBusName,
+				Args: []introspect.Arg{
+					{
+						Name:      slotArgName,
+						Type:      "s",
+						Direction: dbusutil.In,
+					},
+				},
+			},
+			{
+				Name: getStateDBusName,
+				Args: []introspect.Arg{
+					{
+						Name:      slotArgName,
+						Type:      "s",
+						Direction: dbusutil.In,
+					},
+					{
+						Name:      stateArgName,
+						Type:      "s",
+						Direction: dbusutil.Out,
+					},
+				},
+			},
+			{
+				Name: setStateDBusName,
+				Args: []introspect.Arg{
+					{
+						Name:      slotArgName,
+						Type:      "s",
+						Direction: dbusutil.In,
+					},
+					{
+						Name:      stateArgName,
+						Type:      "s",
+						Direction: dbusutil.In,
+					},
+				},
+			},
+			{
+				Name: preInstallDBusName,
+				Args: []introspect.Arg{
+					{
+						Name: contextArgName,
+						// Pass environment block as input.
+						// This allows the context to evolve without changing the API.
+						Type:      "as",
+						Direction: dbusutil.In,
+					},
+				},
+			},
+			{
+				Name: postInstallDBusName,
+				Args: []introspect.Arg{
+					{
+						Name:      contextArgName,
+						Type:      "as",
+						Direction: dbusutil.In,
+					},
+				},
+			},
+		},
+	}
+
+	return reg.RegisterObject(ota.ServiceObjectPath, InterfaceName, methods, metadata)
+}
+
+// GetPrimary maps boothandler.Handler.PrimarySlot to D-Bus.
+func (svc *HostedService) GetPrimary() (slotName string, dbusErr *dbus.Error) {
+	slot, err := svc.bootHandler.PrimarySlot()
+	if err != nil {
+		return "", dbus.MakeFailedError(err)
+	}
+
+	return slot.String(), nil
+}
+
+// SetPrimary maps boothandler.Handler.SetPrimarySlot to D-Bus.
+func (svc *HostedService) SetPrimary(slotName string) (dbusErr *dbus.Error) {
+	var slot boot.Slot
+
+	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if err := svc.bootHandler.SetPrimarySlot(slot); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	return nil
+}
+
+// GetState maps boothandler.Handler.SlotState to D-Bus.
+func (svc *HostedService) GetState(slotName string) (bootState string, dbusErr *dbus.Error) {
+	var slot boot.Slot
+
+	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
+		return "", dbus.MakeFailedError(err)
+	}
+
+	state, err := svc.bootHandler.SlotState(slot)
+	if err != nil {
+		return "", dbus.MakeFailedError(err)
+	}
+
+	return state.String(), nil
+}
+
+// SetState maps boothandler.Handler.SetSlotState to D-Bus.
+func (svc *HostedService) SetState(slotName string, stateName string) (dbusErr *dbus.Error) {
+	var slot boot.Slot
+
+	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	var state boot.SlotState
+
+	if err := state.UnmarshalText([]byte(stateName)); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if err := svc.bootHandler.SetSlotState(slot, state); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	return nil
+}
+
+func (svc *HostedService) populateHandlerPID(ctx *installhandler.HandlerContext, sender dbus.Sender) error {
+	// Find the pid of the sender and store it int the handler context.
+	// See https://dbus.freedesktop.org/doc/dbus-specification.html
+	var pid uint32
+
+	if err := svc.conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, sender).Store(&pid); err != nil {
+		return err
+	}
+
+	ctx.InitialHandlerPID = int(pid)
+
+	return nil
+}
+
+// PreInstall maps installhandler.Participant.PreInstallHandler to D-Bus.
+func (svc *HostedService) PreInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) {
+	ctx, err := installhandler.NewHandlerContext(env)
+	if err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if err := svc.populateHandlerPID(ctx, sender); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if err := svc.installHandler.PreInstallHandler(ctx); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	return nil
+}
+
+// PostInstall maps installhandler.Participant.PostInstallHandler to D-Bus.
+func (svc *HostedService) PostInstall(sender dbus.Sender, env []string) (dbusErr *dbus.Error) {
+	ctx, err := installhandler.NewHandlerContext(env)
+	if err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if err := svc.populateHandlerPID(ctx, sender); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if err := svc.installHandler.PostInstallHandler(ctx); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	return nil
+}
diff --git a/service/service.go b/service/service.go
index c81d8e8102c832364b5815a0c87c97a660828e08..0b625d86c93094c46a5b17216f1da2234605f7bd 100644
--- a/service/service.go
+++ b/service/service.go
@@ -15,9 +15,11 @@ import (
 	"booting.oniroproject.org/distro/components/sysota/dirs"
 	"booting.oniroproject.org/distro/components/sysota/ota"
 	"booting.oniroproject.org/distro/components/sysota/ota/raucadapter"
-	"booting.oniroproject.org/distro/components/sysota/ota/raucinterface"
 	"booting.oniroproject.org/distro/components/sysota/picfg/pimodel"
+	"booting.oniroproject.org/distro/components/sysota/service/boothosted"
+	"booting.oniroproject.org/distro/components/sysota/service/rauchosted"
 	"booting.oniroproject.org/distro/components/sysota/service/testhosted"
+	"booting.oniroproject.org/distro/components/sysota/service/updatehosted"
 )
 
 // RequestBusName requests a well-known name on the bus
@@ -69,20 +71,30 @@ func WithExitChannel(exit chan<- struct{}) Option {
 	}
 }
 
-// Service implements the D-Bus service capable of downloading system updates.
+// Service is implements D-Bus APIs of the SystemOTA device client.
+//
+// Individual parts of the service, modelled as distinct D-Bus interfaces, are
+// implemented as unexported fields. Internally those are UpdateService,
+// raucinterface.Service as well TestService.
+//
+// Service holds unexported PersistentState and PersistentConfig. Usually those
+// should be initialized with by passing WithStateFile and WithConfigFiles to
+// New. They are made available to several hosted services: update, boot, rauc
+// and test hosted services, which implement individual D-Bus interfaces.
 type Service struct {
 	dbusutil.ServiceHost
 
 	stateGuard ota.StateGuard
 	confGuard  ota.ConfigGuard
 
-	updateService UpdateService
-
+	bootProto   boot.Protocol
 	raucAdapter *raucadapter.Adapter
-	raucService *raucinterface.Service
+	exit        chan<- struct{}
 
-	test testhosted.HostedService
-	exit chan<- struct{}
+	boot   boothosted.HostedService
+	update updatehosted.HostedService
+	rauc   rauchosted.HostedService
+	test   testhosted.HostedService
 }
 
 var bootProviders = map[ota.BootLoaderType]func(config *ota.Config) (boot.Protocol, error){
@@ -119,6 +131,8 @@ func configuredBootProtocol(cfgGuard *ota.ConfigGuard) (boot.Protocol, error) {
 	return nil, unsupportedBootLoaderError(cfg.BootLoaderType)
 }
 
+type dbusNotificationKey struct{}
+
 // New returns a new service.
 //
 // In practice you want to call it with both the WithConfigurer WithStater
@@ -126,7 +140,6 @@ func configuredBootProtocol(cfgGuard *ota.ConfigGuard) (boot.Protocol, error) {
 // configuration and state and all changes will be ephemeral.
 func New(conn *dbus.Conn, opts ...Option) (*Service, error) {
 	svc := &Service{}
-	svc.ServiceHost.Init(conn)
 
 	for _, opt := range opts {
 		if err := opt(svc); err != nil {
@@ -134,35 +147,63 @@ func New(conn *dbus.Conn, opts ...Option) (*Service, error) {
 		}
 	}
 
+	svc.stateGuard.RegisterObserver(dbusNotificationKey{}, func(st *ota.SystemState) error {
+		// TODO: send dbus notifications once we expose the BootMode property.
+		return nil
+	})
+
+	svc.ServiceHost.Init(conn)
+
 	bootProto, err := configuredBootProtocol(&svc.confGuard)
 	if err != nil {
 		return nil, err
 	}
 
-	svc.raucAdapter = raucadapter.New(bootProto, svc)
+	svc.bootProto = bootProto
 
-	// Configure and add the boot service if the corresponding
-	// debug flag is enabled.
-	if svc.confGuard.Config().DebugBootAPI {
-		bootService := &BootProtocolService{
-			bootProto: bootProto,
-		}
-		svc.AddHostedService(bootService)
+	svc.setupRaucAdapter()
+	svc.setupBootProtocolService()
+	svc.setupUpdateService()
+	svc.setupRaucService()
+	svc.setupTestService()
+
+	return svc, nil
+}
+
+func (svc *Service) setupRaucAdapter() {
+	// RAUC adapter takes the boot protocol and a boot mode holder which is
+	// implemented by the service directly.
+	svc.raucAdapter = raucadapter.New(svc.bootProto, &svc.stateGuard)
+}
+
+// setupBootProtocolService configures and adds the boot protocol service
+// if the corresponding debug flag is enabled in the configuration system.
+func (svc *Service) setupBootProtocolService() {
+	if !svc.confGuard.Config().DebugBootAPI {
+		return
 	}
 
-	// Configure and add the update service.
-	svc.updateService.maker = "dummy-maker"
-	svc.updateService.model = "dummy-model"
-	svc.updateService.stream = "dummy-stream"
-	svc.AddHostedService(&svc.updateService)
+	svc.boot.Init(svc.bootProto)
+	svc.AddHostedService(&svc.boot)
+}
 
-	// Configure and add the RAUC service
-	svc.raucService = raucinterface.NewService(svc.raucAdapter, svc.raucAdapter)
-	svc.AddHostedService(svc.raucService)
+func (svc *Service) setupUpdateService() {
+	svc.update.Init(&svc.confGuard)
 
-	svc.setupTestService()
+	svc.AddHostedService(&svc.update)
+}
 
-	return svc, nil
+func (svc *Service) setupRaucService() {
+	svc.rauc.Init(svc.raucAdapter, svc.raucAdapter)
+	svc.AddHostedService(&svc.rauc)
+}
+
+// Exit returns the exit channel of the service.
+//
+// The exit channel can be used to send an empty structure to cause the service
+// to stop processing requests and quit.
+func (svc *Service) Exit() chan<- struct{} {
+	return svc.exit
 }
 
 // setupTestService configures and adds the test service
@@ -177,27 +218,7 @@ func (svc *Service) setupTestService() {
 	svc.AddHostedService(&svc.test)
 }
 
-// BootMode implements raucadapter.BootModeHolder and returns the current boot mode.
-func (svc *Service) BootMode() boot.Mode {
-	return svc.stateGuard.State().BootMode
-}
-
-// SetBootMode implements raucadapter.BootModeHolder and sets the new boot mode.
-func (svc *Service) SetBootMode(bootMode boot.Mode) error {
-	return svc.stateGuard.AlterState(func(st *ota.SystemState) (bool, error) {
-		if st.BootMode == bootMode {
-			return false, nil
-		}
-
-		// TODO(zyga): emit the PropertyChanged signal here.
-		// Any errors should be logged and ignored.
-		st.BootMode = bootMode
-
-		return true, nil
-	})
-}
-
 // IsIdle returns true if all the hosted service are idle.
 func (svc *Service) IsIdle() bool {
-	return svc.updateService.IsIdle()
+	return svc.update.IsIdle()
 }
diff --git a/service/service_test.go b/service/service_test.go
index 0dfebde220f27bc63ab816beec44719cdceece32..6ebc46c059453b7a6dbf16d5a7ec4d029a108859 100644
--- a/service/service_test.go
+++ b/service/service_test.go
@@ -65,7 +65,11 @@ func (s *serviceSuite) EnsureService(c *C) {
 }
 
 func (s *serviceSuite) SetUpTest(c *C) {
-	s.config = ota.Config{}
+	s.config = ota.Config{
+		DeviceMaker:  "test-maker",
+		DeviceModel:  "test-model",
+		UpdateStream: "test-stream",
+	}
 	s.state = ota.SystemState{}
 }
 
@@ -85,6 +89,16 @@ func (s *serviceSuite) TearDownTest(c *C) {
 	}
 }
 
+func (s *serviceSuite) TestExitChannel(c *C) {
+	ch := make(chan struct{}, 1)
+	s.EnsureConn(c)
+	svc, err := service.New(s.conn, service.WithExitChannel(ch))
+	c.Assert(err, IsNil)
+	svc.Exit() <- struct{}{}
+	_, ok := <-ch
+	c.Check(ok, Equals, true)
+}
+
 func (s *serviceSuite) TestSmokeAccessWithoutBootProtocolAcrossDBus(c *C) {
 	s.config.DebugBootAPI = true
 	s.EnsureService(c)
@@ -220,15 +234,15 @@ func (s *serviceSuite) TestAccessPropertiesAcrossDBus(c *C) {
 
 	err := svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Maker").Store(&propValue)
 	c.Assert(err, IsNil)
-	c.Check(propValue, Equals, dbus.MakeVariant("dummy-maker"))
+	c.Check(propValue, Equals, dbus.MakeVariant("test-maker"))
 
 	err = svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Model").Store(&propValue)
 	c.Assert(err, IsNil)
-	c.Check(propValue, Equals, dbus.MakeVariant("dummy-model"))
+	c.Check(propValue, Equals, dbus.MakeVariant("test-model"))
 
 	err = svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Stream").Store(&propValue)
 	c.Assert(err, IsNil)
-	c.Check(propValue, Equals, dbus.MakeVariant("dummy-stream"))
+	c.Check(propValue, Equals, dbus.MakeVariant("test-stream"))
 
 	err = svcObject.Call("org.freedesktop.DBus.Properties.Get", 0, "org.oniroproject.sysota1.Service", "Potato").Store(&propValue)
 	c.Assert(err, ErrorMatches, `org.freedesktop.DBus.Properties.Error.PropertyNotFound`)
@@ -241,9 +255,9 @@ func (s *serviceSuite) TestAccessPropertiesAcrossDBus(c *C) {
 	err = svcObject.Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.oniroproject.sysota1.Service").Store(&propMap)
 	c.Assert(err, IsNil)
 	c.Check(propMap, DeepEquals, map[string]dbus.Variant{
-		"Maker":  dbus.MakeVariant("dummy-maker"),
-		"Model":  dbus.MakeVariant("dummy-model"),
-		"Stream": dbus.MakeVariant("dummy-stream"),
+		"Maker":  dbus.MakeVariant("test-maker"),
+		"Model":  dbus.MakeVariant("test-model"),
+		"Stream": dbus.MakeVariant("test-stream"),
 	})
 
 	err = svcObject.Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.oniroproject.sysota1.Potato").Store(&propMap)
@@ -291,14 +305,3 @@ func (s *serviceSuite) TestGetManagedObjects(c *C) {
 	c.Assert(err, IsNil)
 	c.Check(objs, DeepEquals, map[dbus.ObjectPath]map[string]map[string]dbus.Variant{})
 }
-
-func (s *serviceSuite) TestBootMode(c *C) {
-	s.EnsureService(c)
-
-	c.Check(s.svc.BootMode(), Equals, boot.Normal)
-	// TODO(zyga): check that PropertiesChanged signal was *not* sent.
-	c.Check(s.svc.SetBootMode(boot.Mode(boot.Normal)), IsNil)
-	// TODO(zyga): check that PropertiesChanged signal was sent.
-	c.Check(s.svc.SetBootMode(boot.Mode(ota.TestBoot)), IsNil)
-	c.Check(s.svc.BootMode(), Equals, boot.Try)
-}
diff --git a/service/oper/oper.go b/service/updatehosted/oper/oper.go
similarity index 100%
rename from service/oper/oper.go
rename to service/updatehosted/oper/oper.go
diff --git a/service/updateservice.go b/service/updatehosted/updatehosted.go
similarity index 70%
rename from service/updateservice.go
rename to service/updatehosted/updatehosted.go
index 94669f7acf1aac8b3849d40cf92056f5dedd970b..fb507c45e0d596e047ee082f225b52f2429f56f1 100644
--- a/service/updateservice.go
+++ b/service/updatehosted/updatehosted.go
@@ -1,8 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-FileCopyrightText: Huawei Inc.
 
-// Package service implements the sysotad D-Bus service.
-package service
+package updatehosted
 
 import (
 	"fmt"
@@ -15,26 +14,26 @@ import (
 	"booting.oniroproject.org/distro/components/sysota/dbusutil"
 	"booting.oniroproject.org/distro/components/sysota/dbusutil/objmgr"
 	"booting.oniroproject.org/distro/components/sysota/ota"
-	"booting.oniroproject.org/distro/components/sysota/service/oper"
+	"booting.oniroproject.org/distro/components/sysota/service/updatehosted/oper"
 )
 
 const (
 	// UpdateServiceInterfaceName is the name of the SystemOTA Service interface.
 	// TODO(zyga): re-think the name.
-	UpdateServiceInterfaceName = "org.oniroproject.sysota1.Service"
+	InterfaceName = "org.oniroproject.sysota1.Service"
 
 	makerProperty  = "Maker"
 	modelProperty  = "Model"
 	streamProperty = "Stream"
 )
 
-// ServiceIntrospectData is the D-Bus introspection data for Service interface of the service object.
+// UpdateServiceIntrospectData is the D-Bus introspection data for Service interface of the service object.
 //
 // It is provided explicitly without using reflection for a bit more control
 // over the provided data, given that we implement the properties interface
 // ourselves.
-var ServiceIntrospectData = introspect.Interface{
-	Name: UpdateServiceInterfaceName,
+var IntrospectData = introspect.Interface{
+	Name: InterfaceName,
 	Methods: []introspect.Method{
 		{
 			Name: "UpdateStreams",
@@ -92,7 +91,7 @@ var ServiceIntrospectData = introspect.Interface{
 	},
 }
 
-// UpdateService provides functions for downloading and applying updates.
+// HostedService provides functions for downloading and applying updates.
 //
 // Maker is a near-arbitrary string that identifier the device maker.
 // Model is a near-arbitrary string that is selected by the device maker.
@@ -100,21 +99,23 @@ var ServiceIntrospectData = introspect.Interface{
 // branches or stability levels to use.
 //
 // Available updates are specific to a given (maker,model,stream) tuple.
-type UpdateService struct {
-	m    sync.RWMutex
+type HostedService struct {
 	conn *dbus.Conn
 
-	maker  string
-	model  string
-	stream string
-
-	ops []*oper.Operation
+	pc *ota.ConfigGuard
 
+	opsMutex        sync.RWMutex
+	ops             []*oper.Operation
 	lastOperationID int
 }
 
+// Init initializes UpdateService with a given persistent config.
+func (svc *HostedService) Init(pc *ota.ConfigGuard) {
+	svc.pc = pc
+}
+
 // JoinServiceHost integrates with dbusutil.ServiceHost.
-func (svc *UpdateService) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
+func (svc *HostedService) JoinServiceHost(reg dbusutil.ServiceRegistration) error {
 	// Remember connection to send signals.
 	svc.conn = reg.DBusConn()
 
@@ -124,7 +125,7 @@ func (svc *UpdateService) JoinServiceHost(reg dbusutil.ServiceRegistration) erro
 		"UpdateStreams": svc.UpdateStreams,
 		"UpdateDevice":  svc.UpdateDevice,
 	}
-	if err := reg.RegisterObject(path, UpdateServiceInterfaceName, svcMethods, ServiceIntrospectData); err != nil {
+	if err := reg.RegisterObject(path, InterfaceName, svcMethods, IntrospectData); err != nil {
 		return err
 	}
 
@@ -147,57 +148,51 @@ func (svc *UpdateService) JoinServiceHost(reg dbusutil.ServiceRegistration) erro
 	return nil
 }
 
+/*
 // Maker returns the name of the maker of the device.
-func (svc *UpdateService) Maker() string {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
-
-	return svc.maker
+func (svc *HostedService) Maker() string {
+	return svc.pc.Config().DeviceMaker
 }
 
 // Model returns the model name of the device.
-func (svc *UpdateService) Model() string {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
-
-	return svc.model
+func (svc *HostedService) Model() string {
+	return svc.pc.Config().DeviceModel
 }
 
 // Stream returns the name of the update stream the device is subscribed to.
-func (svc *UpdateService) Stream() string {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
-
-	return svc.stream
+func (svc *HostedService) Stream() string {
+	return svc.pc.Config().UpdateStream
 }
+*/
 
-// SetStream sets the new stream name to the given value.
-func (svc *UpdateService) SetStream(stream string) error {
-	svc.m.Lock()
-	defer svc.m.Unlock()
+// setStream sets the new stream name to the given value.
+func (svc *HostedService) setStream(stream string) error {
+	err := svc.pc.AlterConfig(func(cfg *ota.Config) (bool, error) {
+		if cfg.UpdateStream == stream {
+			return false, nil
+		}
 
-	return svc.setStreamUnlocked(stream)
-}
+		// TODO(zyga): check that stream is valid/known.
+		cfg.UpdateStream = stream
+
+		return true, nil
+	})
+	if err != nil {
+		return err
+	}
 
-// setStreamUnlocked sets the new stream name to the given value.
-//
-// It must be called with the write lock held.
-func (svc *UpdateService) setStreamUnlocked(stream string) error {
-	// TODO check that stream is valid/known.
-	svc.stream = stream
-	// TODO: persist the configuration
 	updated := map[string]dbus.Variant{
 		streamProperty: dbus.MakeVariant(stream),
 	}
 	invalidated := []string{}
 
-	return svc.conn.Emit(ota.ServiceObjectPath, dbusutil.PropertiesChangedSignal, UpdateServiceInterfaceName, updated, invalidated)
+	return svc.conn.Emit(ota.ServiceObjectPath, dbusutil.PropertiesChangedSignal, InterfaceName, updated, invalidated)
 }
 
 // IsIdle returns true if the OTA service is idle and can be shut down.
-func (svc *UpdateService) IsIdle() bool {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
+func (svc *HostedService) IsIdle() bool {
+	svc.opsMutex.RLock()
+	defer svc.opsMutex.RUnlock()
 
 	return len(svc.ops) == 0
 }
@@ -210,9 +205,9 @@ func (svc *UpdateService) IsIdle() bool {
 // TODO: Implement this for real by calling thing from the OTA backend.
 // XXX: The return type is probably wrong. It should return "ao"
 // representing the known streams.
-func (svc *UpdateService) UpdateStreams() *dbus.Error {
-	svc.m.Lock()
-	defer svc.m.Unlock()
+func (svc *HostedService) UpdateStreams() *dbus.Error {
+	svc.opsMutex.Lock()
+	defer svc.opsMutex.Unlock()
 
 	fmt.Printf("called UpdateStreams\n")
 
@@ -225,14 +220,14 @@ func (svc *UpdateService) UpdateStreams() *dbus.Error {
 // and returns an object which can be used to track the progress and result.
 //
 // TODO: Implement this for real by calling thing from the OTA backend.
-func (svc *UpdateService) UpdateDevice(hints map[string]string) (dbus.ObjectPath, *dbus.Error) {
-	svc.m.Lock()
-	defer svc.m.Unlock()
+func (svc *HostedService) UpdateDevice(hints map[string]string) (dbus.ObjectPath, *dbus.Error) {
+	svc.opsMutex.Lock()
+	defer svc.opsMutex.Unlock()
 
 	// TODO: implement this for real
 	fmt.Printf("called UpdateDevice with hints %v\n", hints)
 
-	op, err := svc.makeOp()
+	op, err := svc.makeOpUnlocked()
 	if err != nil {
 		return "", dbus.MakeFailedError(err)
 	}
@@ -242,7 +237,7 @@ func (svc *UpdateService) UpdateDevice(hints map[string]string) (dbus.ObjectPath
 	return oper.ObjectPath(op.ID()), nil
 }
 
-func (svc *UpdateService) makeOp() (*oper.Operation, error) {
+func (svc *HostedService) makeOpUnlocked() (*oper.Operation, error) {
 	svc.lastOperationID++
 	op := oper.New(svc.conn, svc.lastOperationID)
 
@@ -273,19 +268,18 @@ func (svc *UpdateService) makeOp() (*oper.Operation, error) {
 // more precise control. We may drop this later, depending on how it gets used.
 
 // Get implements org.freedesktop.DBus.Properties.Get.
-func (svc *UpdateService) Get(iface, property string) (dbus.Variant, *dbus.Error) {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
+func (svc *HostedService) Get(iface, property string) (dbus.Variant, *dbus.Error) {
+	cfg := svc.pc.Config()
 
 	switch iface {
-	case UpdateServiceInterfaceName:
+	case InterfaceName:
 		switch property {
 		case makerProperty:
-			return dbus.MakeVariant(svc.maker), nil
+			return dbus.MakeVariant(cfg.DeviceMaker), nil
 		case modelProperty:
-			return dbus.MakeVariant(svc.model), nil
+			return dbus.MakeVariant(cfg.DeviceModel), nil
 		case streamProperty:
-			return dbus.MakeVariant(svc.stream), nil
+			return dbus.MakeVariant(cfg.UpdateStream), nil
 		default:
 			return dbus.Variant{}, prop.ErrPropNotFound
 		}
@@ -295,16 +289,15 @@ func (svc *UpdateService) Get(iface, property string) (dbus.Variant, *dbus.Error
 }
 
 // GetAll implements org.freedesktop.DBus.Properties.GetAll.
-func (svc *UpdateService) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
+func (svc *HostedService) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error) {
+	cfg := svc.pc.Config()
 
 	switch iface {
-	case UpdateServiceInterfaceName:
+	case InterfaceName:
 		return map[string]dbus.Variant{
-			makerProperty:  dbus.MakeVariant(svc.maker),
-			modelProperty:  dbus.MakeVariant(svc.model),
-			streamProperty: dbus.MakeVariant(svc.stream),
+			makerProperty:  dbus.MakeVariant(cfg.DeviceMaker),
+			modelProperty:  dbus.MakeVariant(cfg.DeviceModel),
+			streamProperty: dbus.MakeVariant(cfg.UpdateStream),
 		}, nil
 	default:
 		return nil, prop.ErrIfaceNotFound
@@ -312,21 +305,20 @@ func (svc *UpdateService) GetAll(iface string) (map[string]dbus.Variant, *dbus.E
 }
 
 // Set implements org.freedesktop.Properties.Set.
-func (svc *UpdateService) Set(iface, property string, newValue dbus.Variant) (err *dbus.Error) {
-	svc.m.Lock()
-	defer svc.m.Unlock()
-
+func (svc *HostedService) Set(iface, property string, newValue dbus.Variant) (err *dbus.Error) {
 	switch iface {
-	case UpdateServiceInterfaceName:
+	case InterfaceName:
 		switch property {
 		case makerProperty, modelProperty:
 			return prop.ErrReadOnly
 		case streamProperty:
-			if newValue.Signature() != dbus.SignatureOf(svc.stream) {
+			var strVal string
+
+			if newValue.Signature() != dbus.SignatureOf(strVal) {
 				return prop.ErrInvalidArg
 			}
 
-			if err := svc.setStreamUnlocked(newValue.Value().(string)); err != nil {
+			if err := svc.setStream(newValue.Value().(string)); err != nil {
 				return dbus.MakeFailedError(err)
 			}
 		default:
@@ -340,9 +332,9 @@ func (svc *UpdateService) Set(iface, property string, newValue dbus.Variant) (er
 }
 
 // GetManagedObjects implements org.freedesktop.DBus.ObjectManager.GetManagedObjects.
-func (svc *UpdateService) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) {
-	svc.m.RLock()
-	defer svc.m.RUnlock()
+func (svc *HostedService) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) {
+	svc.opsMutex.RLock()
+	defer svc.opsMutex.RUnlock()
 
 	objsIfacesProps := make(map[dbus.ObjectPath]map[string]map[string]dbus.Variant, len(svc.ops))