OmniView follows hexagonal architecture (also known as ports and adapters) combined with clean architecture principles. This design isolates business logic from external dependencies, making the system testable, maintainable, and adaptable.
Architecture Layers
The codebase is organized into four primary layers:
internal/
├── core/
│ ├── domain/ # Entities, Value Objects, Business Rules
│ └── ports/ # Interfaces for external interactions
├── service/ # Application services & use cases
├── adapter/ # External system implementations
│ ├── config/
│ └── storage/
│ ├── oracle/ # Oracle DB + AQ implementation
│ └── boltdb/ # Local storage implementation
└── app/ # Application bootstrap
1. Domain Layer (internal/core/domain)
The innermost layer containing pure business logic with zero external dependencies .
Entities - Objects with identity that encapsulate business rules:
type Subscriber struct {
Name string
BatchSize int
WaitTime int
}
type QueueMessage struct {
MessageID string `json:"MESSAGE_ID"`
ProcessName string `json:"PROCESS_NAME"`
LogLevel string `json:"LOG_LEVEL"`
Payload string `json:"PAYLOAD"`
Timestamp string `json:"TIMESTAMP"`
}
Value Objects - Immutable objects representing concepts:
type QueueConfig struct {}
func ( QueueConfig ) Name () string { return "OMNI_TRACER_QUEUE" }
func ( QueueConfig ) TableName () string { return "AQ$OMNI_TRACER_QUEUE" }
func ( QueueConfig ) PayloadType () string { return "OMNI_TRACER_PAYLOAD_TYPE" }
2. Ports Layer (internal/core/ports)
Defines interfaces that specify how the application interacts with external systems. The domain layer never depends on concrete implementations.
// Port for Oracle database operations
type DatabaseRepository interface {
ExecuteStatement ( query string ) error
Fetch ( query string ) ([] string , error )
RegisterNewSubscriber ( subscriber domain . Subscriber ) error
BulkDequeueTracerMessages ( subscriber domain . Subscriber ) ([] string , [][] byte , int , error )
// ... more methods
}
// Port for BoltDB local storage
type ConfigRepository interface {
Initialize () error
SaveDatabaseConfig ( config domain . DatabaseSettings ) error
SetSubscriber ( subscriber domain . Subscriber ) error
GetSubscriber () ( * domain . Subscriber , error )
// ... more methods
}
Dependency Inversion Principle : Services depend on ports (interfaces), not concrete adapters. This allows swapping implementations without changing business logic.
3. Service Layer (internal/service)
Contains application services that orchestrate use cases by coordinating domain objects and ports.
TracerService (service/tracer)
Manages the Oracle AQ message consumption lifecycle.
type TracerService struct {
db ports . DatabaseRepository
bolt ports . ConfigRepository
processMu sync . Mutex
}
func NewTracerService ( db ports . DatabaseRepository , bolt ports . ConfigRepository ) * TracerService {
return & TracerService {
db : db ,
bolt : bolt ,
}
}
Key responsibilities:
Deploy OMNI_TRACER_API package to Oracle
Start blocking consumer loop for message processing
Process batches of trace messages
Handle message deserialization and display
SubscriberService (service/subscribers)
Manages subscriber registration and lifecycle.
subscribers/subscriber_service.go
type SubscriberService struct {
db ports . DatabaseRepository
bolt ports . ConfigRepository
}
// Generates UUID-based subscriber name
func generateSubscriberName () string {
uuidWithHyphen := uuid . New ()
return "SUB_" + strings . ToUpper ( strings . ReplaceAll ( uuidWithHyphen . String (), "-" , "_" ))
}
Key responsibilities:
Generate unique subscriber names (e.g., SUB_A1B2C3D4_E5F6_...)
Register subscribers in Oracle AQ
Persist subscriber info to BoltDB
Retrieve existing subscribers
PermissionService (service/permissions)
Ensures required Oracle database privileges are granted.
permissions/permissions_service.go
type PermissionService struct {
db ports . DatabaseRepository
bolt ports . ConfigRepository
}
Key responsibilities:
Deploy temporary permission check package
Validate Oracle AQ-related privileges
Store permission status in BoltDB
Clean up check package after validation
4. Adapter Layer (internal/adapter)
Provides concrete implementations of port interfaces.
OracleAdapter (adapter/storage/oracle)
Implements DatabaseRepository using CGO/ODPI-C for direct Oracle interaction.
type OracleAdapter struct {
config * domain . DatabaseSettings
Connection * C . dpiConn
Context * C . dpiContext
}
func ( oa * OracleAdapter ) RegisterNewSubscriber ( subscriber domain . Subscriber ) error {
return oa . ExecuteWithParams (
"BEGIN OMNI_TRACER_API.Register_Subscriber(:subscriberName); END;" ,
map [ string ] interface {}{ "subscriberName" : subscriber . Name },
)
}
Includes C implementation for high-performance bulk dequeue operations (see dequeue_ops.c).
BoltAdapter (adapter/storage/boltdb)
Implements ConfigRepository using BoltDB for local key-value storage.
type BoltAdapter struct {
db * bolt . DB
}
Stores application state:
Database connection settings
Subscriber information
Permission check results
First-run status
Dependency Flow
The architecture enforces unidirectional dependency flow:
┌─────────────────────────────────────────────────────┐
│ main.go │
│ (Dependency Injection) │
└──────────────────────┬──────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Services │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Tracer │ │ Subscriber │ │ Permission │ │
│ │ Service │ │ Service │ │ Service │ │
│ └─────┬──────┘ └──────┬───────┘ └──────┬───────┘ │
└────────┼────────────────┼──────────────────┼─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ Ports │
│ (DatabaseRepository, ConfigRepository) │
└────────┬─────────────────────────────────┬───────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ OracleAdapter │ │ BoltAdapter │
│ (CGO/ODPI-C) │ │ (BoltDB KV) │
└─────────────────┘ └─────────────────────┘
Key Principle : Inner layers (domain/ports) never import outer layers (services/adapters). Dependencies always point inward.
Initialization Flow
The application bootstrap in main.go follows this sequence:
func main () {
// 1. Initialize local storage
boltAdapter := boltdb . NewBoltAdapter ( "omniview.bolt" )
boltAdapter . Initialize ()
// 2. Load configuration
cfgLoader := config . NewConfigLoader ( boltAdapter )
appConfig , _ := cfgLoader . LoadClientConfigurations ()
// 3. Connect to Oracle
dbAdapter := oracle . NewOracleAdapter ( appConfig )
dbAdapter . Connect ()
// 4. Initialize services (dependency injection)
permissionService := permissions . NewPermissionService ( dbAdapter , boltAdapter )
tracerService := tracer . NewTracerService ( dbAdapter , boltAdapter )
subscriberService := subscribers . NewSubscriberService ( dbAdapter , boltAdapter )
// 5. Run startup checks
permissionService . DeployAndCheck ( appConfig . Username )
tracerService . DeployAndCheck ()
// 6. Register subscriber
subscriber , _ := subscriberService . RegisterSubscriber ()
// 7. Start event listener
tracerService . StartEventListener ( ctx , & subscriber , appConfig . Username )
}
Benefits of This Architecture
Testability
Services can be tested with mock implementations of ports, no real database needed.
Maintainability
Business logic is isolated from Oracle-specific details and CGO complexity.
Flexibility
Could swap Oracle AQ for Kafka/RabbitMQ by implementing new adapters.
Clear Boundaries
Each layer has a single responsibility with well-defined interfaces.
Next Steps
Message Queuing Learn how Oracle AQ and blocking consumers work
Subscriber Model Understand subscriber registration and message routing