Designing Application Parts
An application part is a one-shot service with a stable identity, declared dependencies, and a dedicated event thread. This page continues from Building an Application from Parts and explains how to design service interfaces and implementations that remain clear across lifecycle and thread boundaries.
Publishing a Stable Service Interface
Clients should depend on a small abstract interface rather than the concrete part class.
The interface publishes a stable ApplicationPartIdentifier that
both clients and the implementation use for lookup.
Identifier names are case-sensitive and should follow a reverse-domain form. They may contain ASCII letters, digits, dots, underscores, and hyphens and have a maximum length of 200 characters. Managers compare their names, so separately obtained identifier objects with the same name address the same service.
/// Public service interface for the catalog storage part.
class CatalogStorage {
public:
// defaults
virtual ~CatalogStorage() = default;
public:
/// Return the stable identifier shared by clients and the implementation.
[[nodiscard]] static auto partIdentifier() -> el::ApplicationPartIdentifierPtr {
static const auto result = el::ApplicationPartIdentifier::create("dev.erbsland.demo.catalog-storage"_el);
return result;
}
/// Describe the prepared storage service.
[[nodiscard]] virtual auto status() const -> el::String = 0;
};
Implementing and Constructing a Part
ApplicationPartWithInterface combines one public service
interface with the lifecycle supplied by ApplicationPart.
The concrete class provides a static create() factory so graph preparation can defer construction until every
registration has been validated.
The factory returns the concrete shared pointer type.
Construction should establish ordinary object invariants, while initialize() performs work that belongs on the
dedicated part thread.
/// Concrete application part providing catalog storage.
class CatalogStoragePart final : public el::ApplicationPartWithInterface<CatalogStorage> {
public:
/// Create one storage part when the manager prepares the dependency graph.
[[nodiscard]] static auto create() -> std::shared_ptr<CatalogStoragePart> {
return std::make_shared<CatalogStoragePart>();
}
public: // implement CatalogStorage
[[nodiscard]] auto status() const -> el::String override { return "catalog storage is ready"_el; }
protected: // implement ApplicationPart
void initialize() override { el::io::printLine("catalog storage initialized"_el); }
void cleanup() noexcept override { el::io::printLine("catalog storage cleaned up"_el); }
};
Declaring Dependencies
A concrete part declares its dependencies through a static dependencies() method.
The default inherited implementation returns an empty list.
Preparation rejects duplicate identifiers, missing or self dependencies, dependency cycles, invalid factories, and
metadata failures before startup begins.
A dependency reaches Running before the dependent part’s initialize() starts.
Shutdown reverses that relationship: dependents stop before the services they use.
The server part below can therefore resolve storage during initialization and release it during cleanup.
/// Concrete server part that starts after catalog storage and accesses it through its public interface.
class CatalogServerPart final : public el::ApplicationPartWithInterface<CatalogServer> {
public:
/// Declare the services that must be running before this part starts.
[[nodiscard]] static auto dependencies() -> el::ApplicationPartIdentifierList {
return el::ApplicationPartIdentifierList{CatalogStorage::partIdentifier()};
}
/// Create one server part when the manager prepares the dependency graph.
[[nodiscard]] static auto create() -> std::shared_ptr<CatalogServerPart> {
return std::make_shared<CatalogServerPart>();
}
public: // implement CatalogServer
[[nodiscard]] auto endpoint() const -> el::String override { return "local catalog endpoint"_el; }
protected: // implement ApplicationPart
void initialize() override {
_storage = partManager().part<CatalogStorage>();
el::io::printLine("catalog server initialized; "_el, _storage->status());
}
void cleanup() noexcept override {
_storage.reset();
el::io::printLine("catalog server cleaned up"_el);
}
private:
std::shared_ptr<CatalogStorage> _storage;
};
Following the Part Lifecycle
Each started part moves through Starting, Running, Stopping, and either Stopped or Failed.
Its initialize(), running(), stopping(), and cleanup() hooks execute on its dedicated thread.
The automaticStart() decision is the exception: the manager evaluates it on the control event source while initial
startup is being planned.
Returning false from automaticStart() leaves the part uninitialized during automatic startup.
Automatic dependents remain inactive as well.
An explicit start(identifier) later starts the requested part and its inactive dependency closure regardless of
those automatic-start choices.
Lifecycles are one-shot. A part that has stopped or failed cannot be restarted in the same prepared manager.
Calling Services Across Threads
A dependency is available before its dependent starts, but ordinary service calls are not automatically synchronized.
A const method that returns immutable state may be safe directly.
Operations that touch event-owned state should post a callback through the service’s events() target or provide
their own locking and completion model.
Do not wait for a manager or part state from the manager control source or from any managed part thread.
Such a wait would prevent the event that satisfies it from running and is rejected with LogicError.
Handling Failures and Shutdown
An exception from a lifecycle hook or an ordinary part event callback enters the manager’s ordered error queue.
The default error action stops the complete graph.
A custom error handler may return Continue to stop the failed branch and its active dependents while unrelated
branches continue.
stopping() must return promptly.
Its default implementation completes shutdown immediately.
A part with asynchronous shutdown work can return after starting that work and call completeStopping() from its
event loop when finished.
Only then does the loop exit and cleanup() run on the dedicated part thread.
The default graceful-shutdown timeout is 30 seconds per part. After a timeout the manager requests the event loop to quit and records a failure, but it cannot safely interrupt user code that never returns. Keep lifecycle hooks and event callbacks non-blocking so shutdown can always make progress.