Skip to content

Facade

The Facade in comby is a central component that integrates various subsystems to handle commands, events, queries, and other operations. It provides a cohesive and configurable structure for managing the interactions between the core elements of a CQRS and event-sourced architecture.

This is the simplest representation of the Facade. It receives Commandsand Queries and forwards them to the appropriate handlers. These carry out the operations and return a response.

Key Features

The Facade is highly customizable through the FacadeOptions configuration. These options allow developers to specify subsystems such as CacheStore, EventStore, CommandStore, DataStore, and Broker, as well as messaging systems like the CommandBus and EventBus. Additional parameters, such as logging levels, read-only modes, and handler behaviors, can be tailored to meet specific application needs.

The Facade supports middleware for Command, Query, and EventHandler. Middleware functions can be added to preprocess or postprocess operations, offering flexibility to extend or modify the default behavior. For instance, logging, validation, or performance monitoring can be implemented as middleware layers.

Domain-specific providers, such as DomainEventProvider, DomainCommandProvider, and DomainQueryProvider, enable the Facade to manage and retrieve domain events, commands, and queries. Repositories like the EventRepository and CommandRepository provide abstractions for event and command persistence, ensuring seamless interaction with the underlying stores.

The Facade also maintains handler maps for Aggregates, Commands, Queries, and Events. These maps ensure that the appropriate handlers are invoked during operations, promoting clear separation of concerns and efficient processing.

Initialization and Configuration

The NewFacade function creates a new instance of the Facade. By default, it initializes in-memory implementations for core components such as CacheStore, EventStore and CommandStore. Developers can override these defaults by passing FacadeOption configurations, such as FacadeWithAppName or FacadeWithEventStore, to customize the behavior of the Facade.

Following options are available for configuring the Facade:

OptionDescription
FacadeWithAppNameSets the application name. (default "app")
FacadeWithInstanceIdConfigures a unique instance ID. (default 1)
FacadeWithIsReadOnlyEnables or disables read-only mode. (for distributed systems)
FacadeWithCacheStoreSets the cache store.
FacadeWithEventStoreSets the event store.
FacadeWithCommandStoreSets the command store.
FacadeWithDataStoreSets the data store.
FacadeWithLogStoreSets the log store.
FacadeWithBrokerSets the broker.
FacadeWithEmailSets the email service.
FacadeWithCommandBusOverrides the command bus.
FacadeWithEventBusOverrides the event bus.
FacadeWithIgnoreEventHandlerDisables event handlers. (useful for testing)
FacadeWithIgnoreCommandPersistenceDisables command persistence. (useful for testing)
go
import "github.com/gradientzero/comby/v2"
import "github.com/gradientzero/comby/v2/store"

// postgres
eventStore = store.NewEventStorePostgres("localhost", "postgres", "postgres", "db", "5432")

// sqlite
commandStore := store.NewCommandStoreSQLite("/path/to/commandStore.db")

// file system
dataStore := store.NewDataStoreFileSystem("/path/to/data/folder")

// create Facade
fc, _ := comby.NewFacade(
  comby.FacadeWithEventStore(eventStore),
  comby.FacadeWithCommandStore(commandStore),
  comby.FacadeWithDataStore(dataStore),
)

Environment Variables

Comby supports the use of environment variables to configure various aspects of the Facade. The following environment variables are available:

  • COMBY_APP_NAME: To set the name of the application (default app)
  • COMBY_INSTANCE_ID: To set the unique instance ID (default 1)
  • COMBY_READONLY: To set the read-only mode for the application (default false)
  • COMBY_METRICS_ENABLED: To enable or disable metrics collection (default false)

Note: When running the application, these variables are ready and applied overruling the already set options in the facade. For example, the application name can be set as a facade option, but if the environment variable COMBY_APP_NAME is set, it will overrule the facade option.

Component Registrations

The facade itself is initially bare. Among other functions, it offers important registration functions to register the individual components such as aggregate, command, query and event handlers in the facade.

MethodDescription
RegisterAggregateRegisters a new aggregate in the Facade, ensuring its identifier is unique, and associates it with its corresponding domain events for event-driven processing.
RegisterCommandHandlerRegisters a command handler in the Facade, validates its uniqueness across domain commands, and adds its domain commands to the system’s DomainCommandProvider.
RegisterQueryHandlerRegisters a query handler in the Facade, ensuring it is unique in its domain and domain queries, and registers its queries in the DomainQueryProvider.
RegisterEventHandlerRegisters an event handler (read model) in the Facade, validating its domain and uniqueness, and associates its domain events in the DomainEventProvider without setting ownership.

Lifecycle and Restoration

The RestoreState method is used to restore the state of the system by invoking the RestoreState method on all registered event handlers that implement the StateRestorer interface. This ensures that the read models and other stateful components are reconstructed from the event history, maintaining consistency and correctness.

go
import "github.com/gradientzero/comby/v2"

// create Facade
fc, _ := comby.NewFacade(...)
// ...
// restore state
if err := fc.RestoreState(); err != nil {
  panic(err)
}

This method carries out a state restoration in the facade. This means that all EventHandlers that implement the StateRestorer interface will be called. The StateRestorer interface is defined as follows:

go
type StateRestorer interface {
	// RestoreState restores the application state.
	//
	// The restoreFromZero parameter determines whether the restoration starts from
	// an initial state (true) or resumes from the last known state (false).
	// Returns a RestorationDoneCh to signal when the restoration process is complete
	// and an error if the process fails.
	RestoreState(ctx context.Context, restoreFromZero bool) (RestorationDoneCh, error)
  // ...
}

This allows the system to restore its state from the event store. For example, Readmodels can restore their current status - either from scratch or using the last known state withing the underlying used EventStore (Postgres, SQLite, etc.). In the admin dashboard there is also the option to have the restoration performed again. The same mechanism is triggered, but restoreFromZero will be always true. In any case, it is up to the developer whether or not to use this parameters.

Command Lifecycle

The command lifecycle in comby is managed by the Facade, which acts as the central coordinator for processing commands. When a command is dispatched to the Facade, it is routed to the appropriate command handler based on the command's domain and type. The command handler then executes the command logic, potentially modifying the system's state or triggering additional operations.

The command lifecycle consists of the following stages:

Stage 1: Command Dispatch

  • Dispatch Command: User sends a command to the Facade via the DispatchCommand method.
  • Subscribe Hook: When ExecuteWaitToFinish is set to true, the command is added to the Facade's internal Hook channel. Users can wait for the command to finish, including event handler execution, using the WaitForCmd method.
  • Serialize Command: The command is serialized into a byte array for distribution (to Broker or CommandBus).
  • Publish To Broker: In Read Only Mode the serialized command is sent to a configured Broker, which forwards it to the correct application instance.
  • Publish To CommandBus: In normal mode, the serialized command is sent to the internal CommandBus for processing.
  • Response: Upon successful dispatch, DispatchCommand returns a RequestUuid, which matches the command’s unique identifier.

Stage 2: Command Processing

  • CommandBus: The serialized command is retrieved from the CommandBus and processed in a new goroutine (executeCommandBytes) with a default timeout.
  • Deserialize Command: The command is deserialized from the byte array.
  • Provide Domain Command: The domain-specific command data is parsed into a runtime model using the DomainCommandProvider and assigned to command's DomainCmd.
  • Execute Command: The complete command is processed at Stage 3, resulting in a number of generated events.
  • Cache Results: The number of generated events is cached in the CacheStore for optional retrieval commands state using the command’s UUID.

Stage 3: Execute Command

  • Domain Command Handler: The appropriate command handler, wrapped with middleware, processes the command and generates a list of new events.
  • Command Store: The processed command is saved in the CommandStore via the CommandRepository.
  • Event Augmentation: New events are enriched with metadata such as InstanceId, TenantUuid, and CommandUuid to associate them with the originating command.
  • Event Store: New events are stored in the EventStore using the EventRepository.
  • Event Bulk: Events are grouped into an EventBulk object, serialized, and prepared for efficient batch processing.
  • Event Bus: The serialized EventBulk is published to the EventBus for further handling by event handlers.
  • Broker: If a Broker is configured, the EventBulk is forwarded for inter-service distribution.
  • Response: The number of events is returned or an error if the command execution fails.

Stage 4: Event Handler Processing

  • EventBus: The serialized EventBulk is retrieved from the EventBus
  • Deserialize Events: The EventBulk is deserialized and passed to the processEventBulk method in a new goroutine with a default timeout.
  • Process Event Bulk: Each serialized event in the EventBulk is handled by the executeEventBytes method.
  • Execute Event Bytes: Event is deserialized, and domain-specific data is provided by the DomainEventProvider. The resulting event is passed to the executeEvent method.
  • Execute Event: Each event is processed by wrapping it with middleware and passing it to the appropriate event handler. The event can be handled by one or more domain-specific event handlers or a catch-all handler that processes all events.
  • Domain-specific event handler: These user-defined handlers are tailored to process specific types of events within a particular domain.
  • Catch-All Handler (AllEvents): Handlers subscribed to the AllEvents event are triggered for every new event, regardless of type. This allows for generic processing of events that do not have a specific handler.
  • Unsubscribe Hook: Once all events are processed, the command associated with the EventBulk is removed from the Hook channel, after Hook sends a signal to the receiver (WaitForCmd) that the command is finished completely.

Query Lifecycle

The query lifecycle outlines the stages involved in retrieving and processing data in the Comby framework. Queries are a critical component of the system, enabling read operations that fetch and transform data without altering the underlying state. Each stage in the lifecycle is designed to ensure efficient data retrieval, consistent projections, and proper handling of query-specific logic.

The query lifecycle consists of the following steps:

  • Dispatch Query: User sends a query to the Facade via the DispatchQuery method.
  • Provide Domain Query: The domain-specific query data is parsed into a runtime model using the DomainQueryProvider and assigned to query's DomainQry.
  • Execute Query: The executeQuery method identifies the appropriate query handler registered for the specific DomainQry. The query is then wrapped with middleware functions that enhance the processing logic.
  • Response: Finally, the query handler is executed, resulting in a domain-specific, user-defined response or an error.

Middleware and Extensions

The Facade supports middleware functions for command, query, and event handlers. These middleware stacks allow developers to introduce custom preprocessing or postprocessing logic, such as authentication, auditing, or additional validations.

Command Handler Middleware

Fullfilling the CommandHandlerMiddlewareFunc signature:

go
// DomainCommandHandlerFunc defines a function type for handling domain-specific commands
type DomainCommandHandlerFunc func(ctx context.Context, cmd Command, domainCmd DomainCmd) ([]Event, error)

// CommandHandlerMiddlewareFunc defines a function type for middleware applied to domain command handlers.
type CommandHandlerMiddlewareFunc func(ctx context.Context, fn DomainCommandHandlerFunc) DomainCommandHandlerFunc
}

Example:

go
func MiddlewareSimpleCommandHandlerFunc(ctx context.Context, fn DomainCommandHandlerFunc) DomainCommandHandlerFunc {
	return func(ctx context.Context, cmd Command, domainCmd DomainCmd) ([]Event, error) {
		fmt.Println("MiddlewareSimpleCommandHandlerFunc BEFORE")
		defer func() {
			fmt.Println("MiddlewareSimpleCommandHandlerFunc AFTER")
		}()
		return fn(ctx, cmd, domainCmd)
	}
}

Query Handler Middleware

Fullfilling the QueryHandlerMiddlewareFunc signature:

go
// DomainQueryHandlerFunc defines a function type for handling domain-specific queries
type DomainQueryHandlerFunc func(ctx context.Context, qry Query, domainQry DomainQry) (QueryResponse, error)

// QueryHandlerMiddlewareFunc defines a middleware function for a DomainQueryHandlerFunc.
type QueryHandlerMiddlewareFunc func(ctx context.Context, fn DomainQueryHandlerFunc) DomainQueryHandlerFunc

Example:

go
func MiddlewareSimpleQueryHandlerFunc(ctx context.Context, fn DomainQueryHandlerFunc) DomainQueryHandlerFunc {
	return func(ctx context.Context, qry Query, domainQry DomainQry) (QueryResponse, error) {
		fmt.Println("MiddlewareSimpleQueryHandlerFunc BEFORE")
		defer func() {
			fmt.Println("MiddlewareSimpleQueryHandlerFunc AFTER")
		}()
		return fn(ctx, qry, domainQry)
	}
}

Event Handler Middleware

Fullfilling the EventHandlerMiddlewareFunc signature:

go
// DomainEventHandlerFunc defines a function type for handling domain-specific events
type DomainEventHandlerFunc func(ctx context.Context, evt Event, domainEvt DomainEvt) error

// EventHandlerMiddlewareFunc defines a middleware function for a DomainEventHandlerFunc.
type EventHandlerMiddlewareFunc func(ctx context.Context, fn DomainEventHandlerFunc) DomainEventHandlerFunc

Example:

go
func MiddlewareSimpleEventDataHandlerFunc(ctx context.Context, fn DomainEventHandlerFunc) DomainEventHandlerFunc {
	return func(ctx context.Context, evt Event, domainEvent DomainEvt) error {
		fmt.Println("MiddlewareSimpleEventDataHandlerFunc BEFORE")
		defer func() {
			fmt.Println("MiddlewareSimpleEventDataHandlerFunc AFTER")
		}()
		return fn(ctx, evt, domainEvent)
	}
}