Skip to main content
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:
domain/subscriber.go
type Subscriber struct {
    Name      string
    BatchSize int
    WaitTime  int
}
domain/queue.go
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:
domain/queue.go
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.
ports/repository.go
// 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.
tracer/tracer_service.go
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.
oracle/oracle_adapter.go
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.
boltdb/bolt_adapter.go
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:
main.go
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

Build docs developers (and LLMs) love