Anemic class that binds functionality
I think what i was missing was a &quot;builder&quot; class that could take care of the construction, and allow the interfaces to be returned.
Not all transport types needed to be &quot;started&quot;, so i left that concern to the builder of the specific bus (i.e. MQTT transport would have different requirements to Kafka for instance).
Any &quot;stopping&quot; can be done on dispose - any one not wanting to receive messages before then can unsubscribe via the ISubscriber interface.
So we build the base MessageBusBuilderBase class:
public abstract class MessageBusBuilderBase
{
    private MessageBusDiagnostics messageBusDiagnostics;
    public bool IsDebugEnabled =&gt; messageBusDiagnostics != null;

    protected abstract Task&lt;ITransport&gt; NewTransportAsync(ILoggerFactory loggerFactory, Func&lt;TransportMessage, Task&gt; receiveMessage);

    public async Task&lt;BuiltInterfaces&gt; CreateAsync(IMessageSerializer[] serializers, ITypeIdentifier typeIdentifier, ILoggerFactory loggerFactory, bool isDebug = false)
    {
        var serializerRegistry = new SerializerRegistry(serializers);
        var receiver = new MessageDeserializer(serializerRegistry, typeIdentifier, loggerFactory.CreateLogger&lt;MessageDeserializer&gt;());
        var transport = await NewTransportAsync(loggerFactory, receiver.ReceiveMessageFromTransport);
        var publisher = new MessagePublisher(serializerRegistry, transport, typeIdentifier);
        var subscriber = new MessageSubscriber(transport);

        // chain up event handler functions
        receiver.OnMessageDecoded = subscriber.ReceiveMessageFromDeserializer;

        if (isDebug)
            this.messageBusDiagnostics = new MessageBusDiagnostics(transport, loggerFactory.CreateLogger&lt;MessageBusDiagnostics&gt;());

        return new BuiltInterfaces(publisher, subscriber);
    }
}

And the specific derived class for MqttMessageBusBuilder:
public class MqttMessageBusBuilder : MessageBusBuilderBase
{
    private readonly MqttSettings settings;
    public MqttMessageBusBuilder(MqttSettings settings)
    {
        this.settings = settings;
    }

    protected override async Task&lt;ITransport&gt; NewTransportAsync(ILoggerFactory loggerFactory, Func&lt;TransportMessage, Task&gt; receiveMessage)
    {
        var logger = loggerFactory.CreateLogger&lt;MqttMessageBusBuilder&gt;();
        logger.LogDebug($&quot;{nameof(MqttMessageBusBuilder)} is creating a new {nameof(MqttConnection)} and will connect to {this.settings.Server}:{this.settings.Port}.&quot;);
        
        var connection = new MqttConnection(this.settings, receiveMessage, loggerFactory.CreateLogger&lt;MqttConnection&gt;());

        await connection.StartAsync();
        logger.LogDebug($&quot;{nameof(MqttConnection)} has been started via {nameof(MqttConnection.StartAsync)}&quot;);

        return connection;
    }
}

With the above, we now have the interfaces chained correctly, and can register the instances with a inversion of control container clearly separating the concerns of publishing and subscribing:
var mqttBusBuilder = new MqttMessageBusBuilder(settings);
var serializers = new [] { MessagePackSerializer };
var idStrategy = new IdentifyUsingTransportHeaders();
mqttBusBuilder
    .Create(serializers, idStrategy, loggerFactory, var out publisher, var out subscriber);

container.RegisterSingleton(publisher);
container.RegisterSingleton(subscriber);

With regards to &quot;stopping&quot; the bus, I could create a BusManager class to manage a list of created busses and call StopAsync on call - alternatively will just perform this transparently by stopping in Dispose() and let that be a concern of the bus itself as i see other busses do not have Start or Stop functions so I shouldn't let that be the driver of my interface design.