You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2022/04/05 02:55:25 UTC

[skywalking-banyandb-java-client] branch main updated: Add measure API for Java Client (#7)

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb-java-client.git


The following commit(s) were added to refs/heads/main by this push:
     new b8b7083  Add measure API for Java Client (#7)
b8b7083 is described below

commit b8b7083a9ee59a715c4f9f5895877c71317d6911
Author: Jiajing LU <lu...@gmail.com>
AuthorDate: Tue Apr 5 10:55:21 2022 +0800

    Add measure API for Java Client (#7)
    
    * update grpc to 1.42.1 and use latest BanyanDB proto API
    * switch to the new tsdb impl
    * remove grouped client
    * add new measure api
    * add tls support and add grpc channel manager
    * add docs and tests for ChannelManager
    * add network error test
    * use blockingStub for query and metadata management
    * add GRPCStreamServiceStatus
    * add CI tests
---
 README.md                                          | 213 ++++++++------
 pom.xml                                            |  84 +++++-
 .../v1/client/AbstractBulkWriteProcessor.java      |  79 ++++++
 .../banyandb/v1/client/AbstractQuery.java          | 144 ++++++++++
 .../banyandb/v1/client/AbstractWrite.java          |  84 ++++++
 .../banyandb/v1/client/BanyanDBClient.java         | 303 +++++++++++++-------
 .../skywalking/banyandb/v1/client/DataPoint.java   |  67 +++++
 .../v1/client/MeasureBulkWriteProcessor.java       |  74 +++++
 .../banyandb/v1/client/MeasureQuery.java           | 135 +++++++++
 ...ueryResponse.java => MeasureQueryResponse.java} |  24 +-
 .../banyandb/v1/client/MeasureWrite.java           |  98 +++++++
 .../skywalking/banyandb/v1/client/Options.java     |  34 ++-
 .../banyandb/v1/client/PairQueryCondition.java     | 199 ++++++-------
 .../skywalking/banyandb/v1/client/RowEntity.java   |  72 +++--
 .../v1/client/StreamBulkWriteProcessor.java        |  97 +++----
 .../skywalking/banyandb/v1/client/StreamQuery.java | 102 ++-----
 .../banyandb/v1/client/StreamQueryResponse.java    |   4 +-
 .../skywalking/banyandb/v1/client/StreamWrite.java |  95 +++----
 .../apache/skywalking/banyandb/v1/client/Tag.java  | 182 ------------
 .../skywalking/banyandb/v1/client/TagAndValue.java |  55 ++--
 .../banyandb/v1/client/TimestampRange.java         |  14 +-
 .../skywalking/banyandb/v1/client/Value.java       | 295 +++++++++++++++++++
 .../v1/client/grpc/GRPCStreamServiceStatus.java    |  70 +++++
 .../v1/client/grpc/HandleExceptionsWith.java       |  55 ++++
 .../banyandb/v1/client/grpc/MetadataClient.java    |  88 ++++++
 .../channel/ChannelFactory.java}                   |  14 +-
 .../v1/client/grpc/channel/ChannelManager.java     | 226 +++++++++++++++
 .../channel/ChannelManagerSettings.java}           |  40 ++-
 .../client/grpc/channel/DefaultChannelFactory.java |  78 ++++++
 .../exception/AbortedException.java}               |  19 +-
 .../exception/AlreadyExistsException.java}         |  30 +-
 .../exception/BanyanDBApiExceptionFactory.java     |  64 +++++
 .../exception/BanyanDBException.java}              |  34 ++-
 .../exception/BanyanDBGrpcApiExceptionFactory.java |  55 ++++
 .../exception/CancelledException.java}             |  31 +-
 .../exception/DataLossException.java}              |  19 +-
 .../exception/DeadlineExceededException.java}      |  30 +-
 .../exception/FailedPreconditionException.java}    |  30 +-
 .../exception/InternalException.java}              |  19 +-
 .../exception/InvalidArgumentException.java}       |  30 +-
 .../exception/InvalidReferenceException.java}      |  35 +--
 .../exception/NotFoundException.java}              |  19 +-
 .../exception/OutOfRangeException.java}            |  30 +-
 .../exception/PermissionDeniedException.java}      |  30 +-
 .../exception/ResourceExhaustedException.java}     |  30 +-
 .../exception/UnauthenticatedException.java}       |  30 +-
 .../exception/UnavailableException.java}           |  30 +-
 .../exception/UnimplementedException.java}         |  30 +-
 .../exception/UnknownException.java}               |  19 +-
 .../banyandb/v1/client/metadata/Catalog.java       |   8 +-
 .../banyandb/v1/client/metadata/Duration.java      | 160 ++++++-----
 .../banyandb/v1/client/metadata/Group.java         |  86 ++++++
 .../v1/client/metadata/GroupMetadataRegistry.java  |  79 ++++++
 .../banyandb/v1/client/metadata/IndexRule.java     | 114 ++++----
 .../v1/client/metadata/IndexRuleBinding.java       |  89 +++---
 .../metadata/IndexRuleBindingMetadataRegistry.java |  69 +++--
 .../client/metadata/IndexRuleMetadataRegistry.java |  72 +++--
 .../banyandb/v1/client/metadata/Measure.java       | 280 ++++++------------
 .../client/metadata/MeasureMetadataRegistry.java   |  71 +++--
 .../banyandb/v1/client/metadata/MetadataCache.java | 124 ++++++++
 .../v1/client/metadata/MetadataClient.java         |  77 -----
 .../banyandb/v1/client/metadata/NamedSchema.java   |  41 +--
 .../banyandb/v1/client/metadata/Stream.java        | 120 ++++----
 .../v1/client/metadata/StreamMetadataRegistry.java |  71 +++--
 .../banyandb/v1/client/metadata/TagFamilySpec.java | 101 ++++---
 .../banyandb/v1/client/util/CopyOnWriteMap.java    | 181 ++++++++++++
 .../Catalog.java => util/IgnoreHashEquals.java}    |  20 +-
 .../banyandb/v1/client/util/PrivateKeyUtil.java    |  80 ++++++
 src/main/proto/banyandb/v1/banyandb-common.proto   |  64 +++++
 ...andb-metadata.proto => banyandb-database.proto} | 192 +++++++------
 src/main/proto/banyandb/v1/banyandb-measure.proto  | 146 ++++++++++
 .../v1/{banyandb.proto => banyandb-model.proto}    |  63 +++--
 src/main/proto/banyandb/v1/banyandb-stream.proto   |  27 +-
 .../v1/client/AbstractBanyanDBClientTest.java      | 312 +++++++++++++++++++++
 .../v1/client/BanyanDBClientMeasureQueryTest.java  | 178 ++++++++++++
 .../v1/client/BanyanDBClientMeasureWriteTest.java  | 122 ++++++++
 ...est.java => BanyanDBClientStreamQueryTest.java} | 181 ++++++------
 ...est.java => BanyanDBClientStreamWriteTest.java} | 153 +++++-----
 .../banyandb/v1/client/BanyanDBClientTestCI.java   |  62 ++++
 .../BanyanDBMeasureQueryIntegrationTests.java      | 100 +++++++
 .../BanyanDBStreamQueryIntegrationTests.java       | 133 +++++++++
 .../banyandb/v1/client/grpc/ExceptionTest.java     |  64 +++++
 .../v1/client/grpc/channel/ChannelManagerTest.java | 151 ++++++++++
 .../client/grpc/channel/FakeChannelFactory.java}   |  39 ++-
 .../client/grpc/channel/FakeMethodDescriptor.java  |  54 ++++
 .../IndexRuleBindingMetadataRegistryTest.java      | 166 ++++-------
 .../metadata/IndexRuleMetadataRegistryTest.java    | 128 ++-------
 .../metadata/MeasureMetadataRegistryTest.java      | 214 ++++----------
 .../metadata/StreamMetadataRegistryTest.java       | 203 ++++----------
 89 files changed, 5597 insertions(+), 2637 deletions(-)

diff --git a/README.md b/README.md
index adeb039..62fd948 100644
--- a/README.md
+++ b/README.md
@@ -14,91 +14,90 @@ The client implement for SkyWalking BanyanDB in Java.
 
 ## Create a client
 
-Create a `BanyanDBClient` with host, port and a user-specified group and then establish a connection.
+Create a `BanyanDBClient` with host, port and then use `connect()` to establish a connection.
 
 ```java
 // use `default` group
-client = new BanyanDBClient("127.0.0.1", 17912, "default");
+client = new BanyanDBClient("127.0.0.1", 17912);
 // to send any request, a connection to the server must be estabilished
-client.connect(channel);
+client.connect();
 ```
 
-## Schema Management
-
-### Stream
+Besides, you may pass a customized options while building a `BanyanDBClient`. Supported
+options are listed below,
 
-Then we may define a stream with customized configurations,
 
-```java
-// build a stream "sw" with 2 shards and ttl equals to 30 days
-Stream s = new Stream("sw", 2, Duration.ofDays(30));
-s.addTagNameAsEntity("service_id").addTagNameAsEntity("service_instance_id").addTagNameAsEntity("state");
-// TagFamily - tagFamily1
-TagFamilySpec tf1 = new TagFamilySpec("tagFamily1"); // tagFamily1 as the name of the tag family
-tf1.addTagSpec(TagFamilySpec.TagSpec.newBinaryTag("data_binary"));
-s.addTagFamilySpec(tf1);
-// TagFamily - tagFamily2
-TagFamilySpec tf2 = new TagFamilySpec("tagFamily2");
-tf2.addTagSpec(TagFamilySpec.TagSpec.newStringTag("trace_id"))
-        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("state"))
-        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_id"));
-s.addTagFamilySpec(tf2);
-// create with the stream schema, client is the BanyanDBClient created above
-s = client.define(s);
-```
+| Option                     | Description                                                          | Default                  |
+|----------------------------|----------------------------------------------------------------------|--------------------------|
+| maxInboundMessageSize      | Max inbound message size                                             | 1024 * 1024 * 50 (~50MB) |
+| deadline                   | Threshold of gRPC blocking query, unit is second                     | 30 (seconds)             |
+| refreshInterval            | Refresh interval for the gRPC channel, unit is second                | 30 (seconds)             |
+| forceReconnectionThreshold | Threshold of force gRPC reconnection if network issue is encountered | 1                        |
+| forceTLS                   | Force use TLS for gRPC                                               | false                    |
+| sslTrustCAPath             | SSL: Trusted CA Path                                                 |                          |
+| sslCertChainPath           | SSL: Cert Chain Path                                                 |                          |
+| sslKeyPath                 | SSL: Cert Key Path                                                   |                          |
 
-For the last line in the code block, a simple API (i.e. `BanyanDBClient.define(Stream)`) is used to define the schema of `Stream`.
-The same works for `Measure` which will be demonstrated later.
+## Schema Management
 
-### IndexRules
+### Stream and index rules
 
-For better search performance, index rules are necessary for `Stream` and `Measure`. You have to
-specify a full list of index rules that would be bounded to the target `Stream` and `Measure`.
+Then we may define a stream with customized configurations. The following example uses `SegmentRecord` in SkyWalking OAP
+as an illustration,
 
 ```java
-// create IndexRule with inverted index type and save it to series store
-IndexRule indexRule = new IndexRule("db.instance", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.SERIES);
-// tag name specifies the indexed tag
-indexRule.addTag("db.instance");
-// create the index rule "db.instance"
-client.defineIndexRules(stream, indexRule);
+// build a stream default(group)/sw(name) with 2 shards and ttl equals to 30 days
+Stream s = Stream.create("default", "sw")
+        // set entities
+        .setEntityRelativeTags("service_id", "service_instance_id", "is_error")
+        // add a tag family "data"
+        .addTagFamily(TagFamilySpec.create("data")
+            .addTagSpec(TagFamilySpec.TagSpec.newBinaryTag("data_binary"))
+            .build())
+        // add a tag family "searchable"
+        .addTagFamily(TagFamilySpec.create("searchable")
+            // create a string tag "trace_id"
+            .addTagSpec(TagFamilySpec.TagSpec.newStringTag("trace_id"))
+            .addTagSpec(TagFamilySpec.TagSpec.newIntTag("is_error"))
+            .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_id"))
+            .build())
+        .build();
+client.define(s);
 ```
 
-For convenience, `BanyanDBClient.defineIndexRule` supports binding multiple index rules with a single call.
-Internally, an `IndexRuleBinding` is created automatically for users, which will be active between `beginAt` and `expireAt`.
-With this shorthand API, the indexRuleBinding object will be active from the time it is created to the far future (i.e. `2099-01-01 00:00:00.000 UTC`).
+For the last line in the code block, a simple API (i.e. `BanyanDBClient.define(Stream)`) is used to define the schema of `Stream`.
+The same works for `Measure` which will be demonstrated later.
 
-### Measure
+### Measure and index rules
 
 `Measure` can also be defined directly with `BanyanDBClient`,
 
 ```java
-// create a measure registry
-// create a new measure schema with 2 shards and ttl 30 days.
-Measure m = new Measure("measure-example", 2, Duration.ofDays(30));
-// set entity
-m.addTagNameAsEntity("service_id").addTagNameAsEntity("service_instance_id").addTagNameAsEntity("state");
-// TagFamily - tagFamilyName
-TagFamilySpec tf = new TagFamilySpec("tagFamilyName");
-tf.addTagSpec(TagFamilySpec.TagSpec.newStringTag("trace_id"))
-    .addTagSpec(TagFamilySpec.TagSpec.newIntTag("state"))
-    .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_id"));
-s.addTagFamilySpec(tf);
-// set interval rules for different scopes
-m.addIntervalRule(Measure.IntervalRule.matchStringLabel("scope", "day", "1d"));
-m.addIntervalRule(Measure.IntervalRule.matchStringLabel("scope", "hour", "1h"));
-m.addIntervalRule(Measure.IntervalRule.matchStringLabel("scope", "minute", "1m"));
-// add field spec
-// compressMethod and encodingMethod can be specified
-m.addFieldSpec(Measure.FieldSpec.newIntField("tps").compressWithZSTD().encodeWithGorilla().build());
+// create a new measure schema with an additional interval
+// the interval is used to specify how frequently to send a data point
+Measure m = Measure.create("sw_metric", "service_cpm_minute", Duration.ofHours(1))
+        // set entity
+        .setEntityRelativeTags("entity_id")
+        // define a tag family "default"
+        .addTagFamily(TagFamilySpec.create("default")
+            .addTagSpec(TagFamilySpec.TagSpec.newIDTag("id"))
+            .addTagSpec(TagFamilySpec.TagSpec.newStringTag("entity_id"))
+            .build())
+        // define field specs
+        // compressMethod and encodingMethod can be specified
+        .addField(Measure.FieldSpec.newIntField("total").compressWithZSTD().encodeWithGorilla().build())
+        .addField(Measure.FieldSpec.newIntField("value").compressWithZSTD().encodeWithGorilla().build())
+        .build();
 // define a measure, as we've mentioned above
-m = client.define(m);
+client.define(m);
 ```
 
 For more APIs usage, refer to test cases and API docs.
 
 ## Query
 
+### Stream
+
 Construct a `StreamQuery` instance with given time-range and other conditions.
 
 > Note: time-range is left-inclusive and right-exclusive.
@@ -110,18 +109,18 @@ For example,
 Instant end = Instant.now();
 Instant begin = end.minus(15, ChronoUnit.MINUTES);
 // with stream schema, group=default, name=sw
-StreamQuery query = new StreamQuery("sw",
+StreamQuery query = new StreamQuery("default", "sw",
         new TimestampRange(begin.toEpochMilli(), end.toEpochMilli()),
         // projection tags which are indexed
-        Arrays.asList("state", "start_time", "duration", "trace_id"));
+        ImmutableSet.of("state", "start_time", "duration", "trace_id"));
 // search for all states
 query.appendCondition(PairQueryCondition.LongQueryCondition.eq("searchable", "state" , 0L));
 // set order by condition
 query.setOrderBy(new StreamQuery.OrderBy("duration", StreamQuery.OrderBy.Type.DESC));
 // set projection for un-indexed tags
-query.setDataProjections(ImmutableList.of("data_binary"));
+query.setDataProjections(ImmutableSet.of("data_binary"));
 // send the query request
-client.queryStreams(query);
+client.query(query);
 ```
 
 After response is returned, `elements` can be fetched,
@@ -131,14 +130,44 @@ StreamQueryResponse resp = client.queryStreams(query);
 List<RowEntity> entities = resp.getElements();
 ```
 
-where `RowEntity` contains `elementId`, `timestamp` and tag families requested.
+Every item `RowEntity` in the list contains `elementId`, `timestamp` and tag families requested.
 
 The `StreamQueryResponse`, `RowEntity`, `TagFamily` and `Tag` (i.e. `TagAndValue`) forms a hierarchical structure, where
 the order of the tag families and containing tags, i.e. indexes of these objects in the List, follow the order specified 
 in the projection condition we've used in the request.
 
+### Measure
+
+For `Measure`, it is similar to the `Stream`,
+
+```java
+// [begin, end) = [ now - 15min, now )
+Instant end = Instant.now();
+Instant begin = end.minus(15, ChronoUnit.MINUTES);
+// with stream schema, group=sw_metrics, name=service_instance_cpm_day
+MeasureQuery query = new MeasureQuery("sw_metrics", "service_instance_cpm_day",
+    new TimestampRange(begin.toEpochMilli(), end.toEpochMilli()),
+    ImmutableSet.of("id", "scope", "service_id"),
+    ImmutableSet.of("total"));
+// query max "total" with group by tag "service_id"
+query.maxBy("total", ImmutableSet.of("service_id"));
+// use conditions
+query.appendCondition(PairQueryCondition.StringQueryCondition.eq("default", "service_id", "abc"));
+// send the query request
+client.query(query);
+```
+
+After response is returned, `dataPoints` can be extracted,
+
+```java
+MeasureQueryResponse resp = client.query(query);
+List<DataPoint> dataPointList = resp.getDataPoints();
+```
+
 ## Write
 
+### Stream
+
 Since grpc bidi streaming is used for write protocol, build a `StreamBulkWriteProcessor` which would handle back-pressure for you.
 Adjust `maxBulkSize`, `flushInterval` and `concurrency` of the consumer in different scenarios to meet requirements.
 
@@ -155,31 +184,47 @@ the order of tags must exactly be the same with that defined in the schema.
 And the non-existing tags must be fulfilled (with NullValue) instead of compacting all non-null tag values.
 
 ```java
-StreamWrite streamWrite = StreamWrite.builder()
-    .elementId(segmentId)
-    // write binary data to "data" tag family
-    .dataTag(Tag.binaryField(byteData))
-    .timestamp(now.toEpochMilli())
-    .name("sw")
-    // write indexed tags to "searchable" tag family
-    .tag(Tag.stringField(traceId))
-    .tag(Tag.stringField(serviceId))
-    .tag(Tag.stringField(serviceInstanceId))
-    .tag(Tag.stringField(endpointId))
-    .tag(Tag.longField(latency))
-    .tag(Tag.longField(state))
-    .tag(Tag.stringField(httpStatusCode))
-    .tag(Tag.nullField())
-    .tag(Tag.stringField(dbType))
-    .tag(Tag.stringField(dbInstance))
-    .tag(Tag.stringField(broker))
-    .tag(Tag.stringField(topic))
-    .tag(Tag.stringField(queue))
-    .build();
+StreamWrite streamWrite = new StreamWrite("default", "sw", segmentId, now.toEpochMilli())
+    .tag("data_binary", Value.binaryTagValue(byteData))
+    .tag("trace_id", Value.stringTagValue(traceId)) // 0
+    .tag("state", Value.longTagValue(state)) // 1
+    .tag("service_id", Value.stringTagValue(serviceId)) // 2
+    .tag("service_instance_id", Value.stringTagValue(serviceInstanceId)) // 3
+    .tag("endpoint_id", Value.stringTagValue(endpointId)) // 4
+    .tag("duration", Value.longTagValue(latency)) // 5
+    .tag("http.method", Value.stringTagValue(null)) // 6
+    .tag("status_code", Value.stringTagValue(httpStatusCode)) // 7
+    .tag("db.type", Value.stringTagValue(dbType)) // 8
+    .tag("db.instance", Value.stringTagValue(dbInstance)) // 9
+    .tag("mq.broker", Value.stringTagValue(broker)) // 10
+    .tag("mq.topic", Value.stringTagValue(topic)) // 11
+    .tag("mq.queue", Value.stringTagValue(queue)); // 12
 
 streamBulkWriteProcessor.add(streamWrite);
 ```
 
+### Measure
+
+The writing procedure for `Measure` is similar to the above described process and leverages the bidirectional streaming of gRPC,
+
+```java
+// build a MeasureBulkWriteProcessor from client
+MeasureBulkWriteProcessor bulkWriteProcessor = client.buildMeasureWriteProcessor(maxBulkSize, flushInterval, concurrency);
+```
+
+A `BulkWriteProcessor` is created by calling `buildMeasureWriteProcessor`. Then build the `MeasureWrite` object and send with bulk processor,
+
+```java
+Instant now = Instant.now();
+MeasureWrite measureWrite = new MeasureWrite("sw_metric", "service_cpm_minute", now.toEpochMilli());
+    measureWrite.tag("id", TagAndValue.idTagValue("1"))
+    .tag("entity_id", TagAndValue.stringTagValue("entity_1"))
+    .field("total", TagAndValue.longFieldValue(100))
+    .field("value", TagAndValue.longFieldValue(1));
+
+measureBulkWriteProcessor.add(measureWrite);
+```
+
 # Compiling project
 > ./mvnw clean package
 
diff --git a/pom.xml b/pom.xml
index 2cc641c..06d6109 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,25 +74,29 @@
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <compiler.version>1.8</compiler.version>
-        <powermock.version>2.0.7</powermock.version>
+        <powermock.version>2.0.9</powermock.version>
         <checkstyle.version>6.18</checkstyle.version>
-        <junit.version>4.12</junit.version>
+        <junit.version>4.13.2</junit.version>
         <mockito-core.version>3.5.13</mockito-core.version>
-        <lombok.version>1.18.20</lombok.version>
+        <lombok.version>1.18.22</lombok.version>
 
         <!-- core lib dependency -->
         <bytebuddy.version>1.10.19</bytebuddy.version>
-        <grpc.version>1.32.1</grpc.version>
+        <!-- grpc version should align with the Skywalking main repo -->
+        <grpc.version>1.43.2</grpc.version>
+        <protobuf.version>3.19.2</protobuf.version>
+        <protoc.version>3.19.2</protoc.version>
         <gson.version>2.8.6</gson.version>
         <os-maven-plugin.version>1.6.2</os-maven-plugin.version>
         <protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
-        <com.google.protobuf.protoc.version>3.12.0</com.google.protobuf.protoc.version>
-        <protoc-gen-grpc-java.plugin.version>1.32.1</protoc-gen-grpc-java.plugin.version>
-        <netty-tcnative-boringssl-static.version>2.0.39.Final</netty-tcnative-boringssl-static.version>
+        <netty.tcnative.version>2.0.46.Final</netty.tcnative.version>
         <javax.annotation-api.version>1.3.2</javax.annotation-api.version>
+        <auto-value.version>1.9</auto-value.version>
+        <testcontainers.version>1.16.3</testcontainers.version>
+        <awaitility.version>4.2.0</awaitility.version>
         <!-- necessary for Java 9+ -->
         <org.apache.tomcat.annotations-api.version>6.0.53</org.apache.tomcat.annotations-api.version>
-        <slf4j.version>1.7.30</slf4j.version>
+        <slf4j.version>1.7.36</slf4j.version>
 
         <!-- Plugin versions -->
         <docker.plugin.version>0.4.13</docker.plugin.version>
@@ -117,26 +121,35 @@
 
     </properties>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>io.grpc</groupId>
+                <artifactId>grpc-bom</artifactId>
+                <version>${grpc.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <dependencies>
         <dependency>
             <groupId>io.grpc</groupId>
             <artifactId>grpc-netty-shaded</artifactId>
-            <version>${grpc.version}</version>
         </dependency>
         <dependency>
             <groupId>io.grpc</groupId>
             <artifactId>grpc-protobuf</artifactId>
-            <version>${grpc.version}</version>
         </dependency>
         <dependency>
             <groupId>io.grpc</groupId>
             <artifactId>grpc-stub</artifactId>
-            <version>${grpc.version}</version>
         </dependency>
         <dependency>
             <groupId>io.netty</groupId>
             <artifactId>netty-tcnative-boringssl-static</artifactId>
-            <version>${netty-tcnative-boringssl-static.version}</version>
+            <version>${netty.tcnative.version}</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
@@ -150,10 +163,16 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.google.auto.value</groupId>
+            <artifactId>auto-value-annotations</artifactId>
+            <version>${auto-value.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
         <dependency>
             <groupId>io.grpc</groupId>
             <artifactId>grpc-testing</artifactId>
-            <version>${grpc.version}</version>
             <scope>test</scope>
         </dependency>
 
@@ -163,6 +182,24 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
+            <version>${testcontainers.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
@@ -227,11 +264,11 @@
                       protobuf-java version that grpc depends on.
                     -->
                     <protocArtifact>
-                        com.google.protobuf:protoc:${com.google.protobuf.protoc.version}:exe:${os.detected.classifier}
+                        com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
                     </protocArtifact>
                     <pluginId>grpc-java</pluginId>
                     <pluginArtifact>
-                        io.grpc:protoc-gen-grpc-java:${protoc-gen-grpc-java.plugin.version}:exe:${os.detected.classifier}
+                        io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
                     </pluginArtifact>
                 </configuration>
                 <executions>
@@ -275,6 +312,23 @@
                     <source>${compiler.version}</source>
                     <target>${compiler.version}</target>
                     <encoding>${project.build.sourceEncoding}</encoding>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                        <path>
+                            <groupId>com.github.reggar</groupId>
+                            <artifactId>auto-value-ignore-hash-equals</artifactId>
+                            <version>1.1.4</version>
+                        </path>
+                        <path>
+                            <groupId>com.google.auto.value</groupId>
+                            <artifactId>auto-value</artifactId>
+                            <version>${auto-value.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
                 </configuration>
             </plugin>
             <plugin>
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractBulkWriteProcessor.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractBulkWriteProcessor.java
new file mode 100644
index 0000000..4428d70
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractBulkWriteProcessor.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import io.grpc.stub.AbstractAsyncStub;
+import io.grpc.stub.StreamObserver;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.banyandb.v1.client.grpc.GRPCStreamServiceStatus;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public abstract class AbstractBulkWriteProcessor<REQ extends com.google.protobuf.GeneratedMessageV3,
+        STUB extends AbstractAsyncStub<STUB>> extends BulkWriteProcessor {
+    private final STUB stub;
+
+    /**
+     * Create the processor.
+     *
+     * @param stub          an implementation of {@link AbstractAsyncStub}
+     * @param processorName name of the processor for logging
+     * @param maxBulkSize   the max bulk size for the flush operation
+     * @param flushInterval if given maxBulkSize is not reached in this period, the flush would be trigger
+     *                      automatically. Unit is second.
+     * @param concurrency   the number of concurrency would run for the flush max.
+     */
+    protected AbstractBulkWriteProcessor(STUB stub, String processorName, int maxBulkSize, int flushInterval, int concurrency) {
+        super(processorName, maxBulkSize, flushInterval, concurrency);
+        this.stub = stub;
+    }
+
+    /**
+     * Add the measure to the bulk processor.
+     *
+     * @param writeEntity to add.
+     */
+    public void add(AbstractWrite<REQ> writeEntity) {
+        this.buffer.produce(writeEntity);
+    }
+
+    @Override
+    protected void flush(List data) {
+        final GRPCStreamServiceStatus status = new GRPCStreamServiceStatus(false);
+        final StreamObserver<REQ> writeRequestStreamObserver
+                = this.buildStreamObserver(stub.withDeadlineAfter(flushInterval, TimeUnit.SECONDS), status);
+
+        try {
+            data.forEach(write -> {
+                REQ request = ((AbstractWrite<REQ>) write).build();
+                writeRequestStreamObserver.onNext(request);
+            });
+        } catch (Throwable t) {
+            log.error("Transform and send request to BanyanDB fail.", t);
+        } finally {
+            writeRequestStreamObserver.onCompleted();
+        }
+
+        status.wait4Finish();
+    }
+
+    protected abstract StreamObserver<REQ> buildStreamObserver(STUB stub, GRPCStreamServiceStatus status);
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractQuery.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractQuery.java
new file mode 100644
index 0000000..fedb1fe
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractQuery.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.InvalidReferenceException;
+import org.apache.skywalking.banyandb.v1.client.metadata.MetadataCache;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public abstract class AbstractQuery<T> {
+    /**
+     * Group of the current entity
+     */
+    protected final String group;
+    /**
+     * Owner name of the current entity
+     */
+    protected final String name;
+    /**
+     * The time range for query.
+     */
+    protected final TimestampRange timestampRange;
+    /**
+     * Query conditions.
+     */
+    protected final List<PairQueryCondition<?>> conditions;
+    /**
+     * The projections of query result.
+     * These should have defined in the schema.
+     */
+    protected final Set<String> tagProjections;
+
+    @Getter(AccessLevel.PACKAGE)
+    protected final MetadataCache.EntityMetadata metadata;
+
+    public AbstractQuery(String group, String name, TimestampRange timestampRange, Set<String> tagProjections) {
+        this.group = group;
+        this.name = name;
+        this.timestampRange = timestampRange;
+        this.conditions = new ArrayList<>(10);
+        this.tagProjections = tagProjections;
+        this.metadata = MetadataCache.INSTANCE.findMetadata(this.group, this.name);
+    }
+
+    /**
+     * Fluent API for appending query condition
+     *
+     * @param condition the query condition to be appended
+     */
+    public AbstractQuery<T> appendCondition(PairQueryCondition<?> condition) {
+        this.conditions.add(condition);
+        return this;
+    }
+
+    /**
+     * @return QueryRequest for gRPC level query.
+     * @throws BanyanDBException thrown from entity build, e.g. invalid reference to non-exist fields or tags.
+     */
+    abstract T build() throws BanyanDBException;
+
+    protected BanyandbCommon.Metadata buildMetadata() {
+        return BanyandbCommon.Metadata.newBuilder()
+                .setGroup(group)
+                .setName(name)
+                .build();
+    }
+
+    protected List<BanyandbModel.Criteria> buildCriteria() throws BanyanDBException {
+        List<BanyandbModel.Criteria> criteriaList = new ArrayList<>();
+        // set conditions grouped by tagFamilyName
+        Map<String, List<PairQueryCondition<?>>> groupedConditions = new HashMap<>();
+        for (final PairQueryCondition<?> condition : conditions) {
+            String tagFamilyName = metadata.findTagInfo(condition.getTagName()).orElseThrow(() ->
+                    InvalidReferenceException.fromInvalidTag(condition.getTagName())
+            ).getTagFamilyName();
+            List<PairQueryCondition<?>> conditionList = groupedConditions.computeIfAbsent(tagFamilyName, key -> new ArrayList<>());
+            conditionList.add(condition);
+        }
+
+        for (final Map.Entry<String, List<PairQueryCondition<?>>> tagFamily : groupedConditions.entrySet()) {
+            final List<BanyandbModel.Condition> conditionList = tagFamily.getValue().stream().map(PairQueryCondition::build)
+                    .collect(Collectors.toList());
+            BanyandbModel.Criteria criteria = BanyandbModel.Criteria
+                    .newBuilder()
+                    .setTagFamilyName(tagFamily.getKey())
+                    .addAllConditions(conditionList).build();
+            criteriaList.add(criteria);
+        }
+        return criteriaList;
+    }
+
+    protected BanyandbModel.TagProjection buildTagProjections() throws BanyanDBException {
+        return this.buildTagProjections(this.tagProjections);
+    }
+
+    protected BanyandbModel.TagProjection buildTagProjections(Iterable<String> tagProjections) throws BanyanDBException {
+        final ListMultimap<String, String> projectionMap = ArrayListMultimap.create();
+        for (final String tagName : tagProjections) {
+            final Optional<MetadataCache.TagInfo> tagInfo = this.metadata.findTagInfo(tagName);
+            if (!tagInfo.isPresent()) {
+                throw InvalidReferenceException.fromInvalidTag(tagName);
+            }
+            projectionMap.put(tagInfo.get().getTagFamilyName(), tagName);
+        }
+
+        final BanyandbModel.TagProjection.Builder b = BanyandbModel.TagProjection.newBuilder();
+        for (final String tagFamilyName : projectionMap.keySet()) {
+            b.addTagFamilies(BanyandbModel.TagProjection.TagFamily.newBuilder()
+                    .setName(tagFamilyName)
+                    .addAllTags(projectionMap.get(tagFamilyName))
+                    .build());
+        }
+        return b.build();
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractWrite.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractWrite.java
new file mode 100644
index 0000000..7e10132
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/AbstractWrite.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.protobuf.Timestamp;
+import lombok.Getter;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.InvalidReferenceException;
+import org.apache.skywalking.banyandb.v1.client.metadata.MetadataCache;
+import org.apache.skywalking.banyandb.v1.client.metadata.Serializable;
+
+import java.util.Optional;
+
+public abstract class AbstractWrite<P extends com.google.protobuf.GeneratedMessageV3> {
+    /**
+     * Group name of the current entity
+     */
+    @Getter
+    protected final String group;
+    /**
+     * Owner name of the current entity
+     */
+    @Getter
+    protected final String name;
+    /**
+     * Timestamp represents the time of current stream
+     * in the timeunit of milliseconds.
+     */
+    @Getter
+    protected final long timestamp;
+
+    protected final Object[] tags;
+
+    protected final MetadataCache.EntityMetadata entityMetadata;
+
+    public AbstractWrite(String group, String name, long timestamp) {
+        this.group = group;
+        this.name = name;
+        this.timestamp = timestamp;
+        this.entityMetadata = MetadataCache.INSTANCE.findMetadata(group, name);
+        if (this.entityMetadata == null) {
+            throw new IllegalArgumentException("metadata not found");
+        }
+        this.tags = new Object[this.entityMetadata.getTotalTags()];
+    }
+
+    public AbstractWrite<P> tag(String tagName, Serializable<BanyandbModel.TagValue> tagValue) throws BanyanDBException {
+        final Optional<MetadataCache.TagInfo> tagInfo = this.entityMetadata.findTagInfo(tagName);
+        if (!tagInfo.isPresent()) {
+            throw InvalidReferenceException.fromInvalidTag(tagName);
+        }
+        this.tags[tagInfo.get().getOffset()] = tagValue;
+        return this;
+    }
+
+    P build() {
+        BanyandbCommon.Metadata metadata = BanyandbCommon.Metadata.newBuilder()
+                .setGroup(this.group).setName(this.name).build();
+        Timestamp ts = Timestamp.newBuilder()
+                .setSeconds(timestamp / 1000)
+                .setNanos((int) (timestamp % 1000 * 1_000_000)).build();
+        return build(metadata, ts);
+    }
+
+    protected abstract P build(BanyandbCommon.Metadata metadata, Timestamp ts);
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java
index efd8ef0..70d9a68 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClient.java
@@ -20,37 +20,56 @@ package org.apache.skywalking.banyandb.v1.client;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import io.grpc.Channel;
 import io.grpc.ManagedChannel;
-import io.grpc.ManagedChannelBuilder;
-import io.grpc.NameResolverRegistry;
-import io.grpc.internal.DnsNameResolverProvider;
-import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
 
 import java.io.Closeable;
 import java.io.IOException;
-import java.time.ZonedDateTime;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
 
 import io.grpc.stub.StreamObserver;
+import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.measure.v1.MeasureServiceGrpc;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
+import org.apache.skywalking.banyandb.stream.v1.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.HandleExceptionsWith;
+import org.apache.skywalking.banyandb.v1.client.grpc.channel.ChannelManager;
+import org.apache.skywalking.banyandb.v1.client.grpc.channel.DefaultChannelFactory;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Group;
+import org.apache.skywalking.banyandb.v1.client.metadata.GroupMetadataRegistry;
 import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
 import org.apache.skywalking.banyandb.v1.client.metadata.IndexRuleBinding;
 import org.apache.skywalking.banyandb.v1.client.metadata.IndexRuleBindingMetadataRegistry;
 import org.apache.skywalking.banyandb.v1.client.metadata.IndexRuleMetadataRegistry;
 import org.apache.skywalking.banyandb.v1.client.metadata.Measure;
 import org.apache.skywalking.banyandb.v1.client.metadata.MeasureMetadataRegistry;
+import org.apache.skywalking.banyandb.v1.client.metadata.MetadataCache;
 import org.apache.skywalking.banyandb.v1.client.metadata.Stream;
 import org.apache.skywalking.banyandb.v1.client.metadata.StreamMetadataRegistry;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
-import org.apache.skywalking.banyandb.v1.stream.StreamServiceGrpc;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 
 /**
- * BanyanDBClient represents a client instance interacting with BanyanDB server. This is built on the top of BanyanDB v1
- * gRPC APIs.
+ * BanyanDBClient represents a client instance interacting with BanyanDB server.
+ * This is built on the top of BanyanDB v1 gRPC APIs.
+ *
+ * <pre>{@code
+ * // use `default` group
+ * client = new BanyanDBClient("127.0.0.1", 17912);
+ * // to send any request, a connection to the server must be estabilished
+ * client.connect();
+ * }</pre>
  */
 @Slf4j
 public class BanyanDBClient implements Closeable {
@@ -62,26 +81,36 @@ public class BanyanDBClient implements Closeable {
      * The port of BanyanDB server.
      */
     private final int port;
-    /**
-     * The instance name.
-     */
-    private final String group;
     /**
      * Options for server connection.
      */
-    private Options options;
+    @Getter(value = AccessLevel.PACKAGE)
+    private final Options options;
     /**
      * gRPC connection.
      */
+    @Getter(value = AccessLevel.PACKAGE)
     private volatile Channel channel;
     /**
      * gRPC client stub
      */
-    private volatile StreamServiceGrpc.StreamServiceStub streamServiceStub;
+    @Getter(value = AccessLevel.PACKAGE)
+    private StreamServiceGrpc.StreamServiceStub streamServiceStub;
+    /**
+     * gRPC client stub
+     */
+    @Getter(value = AccessLevel.PACKAGE)
+    private MeasureServiceGrpc.MeasureServiceStub measureServiceStub;
+    /**
+     * gRPC future stub.
+     */
+    @Getter(value = AccessLevel.PACKAGE)
+    private StreamServiceGrpc.StreamServiceBlockingStub streamServiceBlockingStub;
     /**
-     * gRPC blocking stub.
+     * gRPC future stub.
      */
-    private volatile StreamServiceGrpc.StreamServiceBlockingStub streamServiceBlockingStub;
+    @Getter(value = AccessLevel.PACKAGE)
+    private MeasureServiceGrpc.MeasureServiceBlockingStub measureServiceBlockingStub;
     /**
      * The connection status.
      */
@@ -89,55 +118,47 @@ public class BanyanDBClient implements Closeable {
     /**
      * A lock to control the race condition in establishing and disconnecting network connection.
      */
-    private volatile ReentrantLock connectionEstablishLock;
+    private final ReentrantLock connectionEstablishLock;
 
     /**
-     * Create a BanyanDB client instance
+     * Create a BanyanDB client instance with a default options.
      *
-     * @param host  IP or domain name
-     * @param port  Server port
-     * @param group Database instance name
+     * @param host IP or domain name
+     * @param port Server port
      */
-    public BanyanDBClient(final String host, final int port, final String group) {
-        this(host, port, group, new Options());
+    public BanyanDBClient(String host, int port) {
+        this(host, port, new Options());
     }
 
     /**
-     * Create a BanyanDB client instance with custom options
+     * Create a BanyanDB client instance with a customized options.
      *
      * @param host    IP or domain name
      * @param port    Server port
-     * @param group   Database instance name
-     * @param options for database connection
+     * @param options customized options
      */
-    public BanyanDBClient(final String host,
-                          final int port,
-                          final String group,
-                          final Options options) {
+    public BanyanDBClient(String host, int port, Options options) {
         this.host = host;
         this.port = port;
-        this.group = group;
         this.options = options;
         this.connectionEstablishLock = new ReentrantLock();
-
-        NameResolverRegistry.getDefaultRegistry().register(new DnsNameResolverProvider());
     }
 
     /**
-     * Connect to the server.
+     * Construct a connection to the server.
      *
-     * @throws RuntimeException if server is not reachable.
+     * @throws IOException thrown if fail to create a connection
      */
-    public void connect() {
+    public void connect() throws IOException {
         connectionEstablishLock.lock();
         try {
             if (!isConnected) {
-                final ManagedChannelBuilder<?> nettyChannelBuilder = NettyChannelBuilder.forAddress(host, port).usePlaintext();
-                nettyChannelBuilder.maxInboundMessageSize(options.getMaxInboundMessageSize());
-
-                channel = nettyChannelBuilder.build();
-                streamServiceStub = StreamServiceGrpc.newStub(channel);
-                streamServiceBlockingStub = StreamServiceGrpc.newBlockingStub(channel);
+                this.channel = ChannelManager.create(this.options.buildChannelManagerSettings(),
+                        new DefaultChannelFactory(this.host, this.port, this.options));
+                streamServiceBlockingStub = StreamServiceGrpc.newBlockingStub(this.channel);
+                measureServiceBlockingStub = MeasureServiceGrpc.newBlockingStub(this.channel);
+                streamServiceStub = StreamServiceGrpc.newStub(this.channel);
+                measureServiceStub = MeasureServiceGrpc.newStub(this.channel);
                 isConnected = true;
             }
         } finally {
@@ -145,21 +166,16 @@ public class BanyanDBClient implements Closeable {
         }
     }
 
-    /**
-     * Connect to the mock server.
-     * Created for testing purpose.
-     *
-     * @param channel the channel used for communication.
-     *                For tests, it is normally an in-process channel.
-     */
     @VisibleForTesting
-    public void connect(Channel channel) {
+    void connect(Channel channel) {
         connectionEstablishLock.lock();
         try {
             if (!isConnected) {
                 this.channel = channel;
-                streamServiceStub = StreamServiceGrpc.newStub(channel);
-                streamServiceBlockingStub = StreamServiceGrpc.newBlockingStub(channel);
+                streamServiceBlockingStub = StreamServiceGrpc.newBlockingStub(this.channel);
+                measureServiceBlockingStub = MeasureServiceGrpc.newBlockingStub(this.channel);
+                streamServiceStub = StreamServiceGrpc.newStub(this.channel);
+                measureServiceStub = MeasureServiceGrpc.newStub(this.channel);
                 isConnected = true;
             }
         } finally {
@@ -173,8 +189,11 @@ public class BanyanDBClient implements Closeable {
      * @param streamWrite the entity to be written
      */
     public void write(StreamWrite streamWrite) {
+        checkState(this.streamServiceStub != null, "stream service is null");
+
         final StreamObserver<BanyandbStream.WriteRequest> writeRequestStreamObserver
-                = streamServiceStub
+                = this.streamServiceStub
+                .withDeadlineAfter(this.getOptions().getDeadline(), TimeUnit.SECONDS)
                 .write(
                         new StreamObserver<BanyandbStream.WriteResponse>() {
                             @Override
@@ -191,7 +210,7 @@ public class BanyanDBClient implements Closeable {
                             }
                         });
         try {
-            writeRequestStreamObserver.onNext(streamWrite.build(group));
+            writeRequestStreamObserver.onNext(streamWrite.build());
         } finally {
             writeRequestStreamObserver.onCompleted();
         }
@@ -207,7 +226,24 @@ public class BanyanDBClient implements Closeable {
      * @return stream bulk write processor
      */
     public StreamBulkWriteProcessor buildStreamWriteProcessor(int maxBulkSize, int flushInterval, int concurrency) {
-        return new StreamBulkWriteProcessor(group, streamServiceStub, maxBulkSize, flushInterval, concurrency);
+        checkState(this.streamServiceStub != null, "stream service is null");
+
+        return new StreamBulkWriteProcessor(this.streamServiceStub, maxBulkSize, flushInterval, concurrency);
+    }
+
+    /**
+     * Create a build process for measure write.
+     *
+     * @param maxBulkSize   the max bulk size for the flush operation
+     * @param flushInterval if given maxBulkSize is not reached in this period, the flush would be trigger
+     *                      automatically. Unit is second
+     * @param concurrency   the number of concurrency would run for the flush max
+     * @return stream bulk write processor
+     */
+    public MeasureBulkWriteProcessor buildMeasureWriteProcessor(int maxBulkSize, int flushInterval, int concurrency) {
+        checkState(this.measureServiceStub != null, "measure service is null");
+
+        return new MeasureBulkWriteProcessor(this.measureServiceStub, maxBulkSize, flushInterval, concurrency);
     }
 
     /**
@@ -216,62 +252,89 @@ public class BanyanDBClient implements Closeable {
      * @param streamQuery condition for query
      * @return hint streams.
      */
-    public StreamQueryResponse queryStreams(StreamQuery streamQuery) {
-        final BanyandbStream.QueryResponse response = streamServiceBlockingStub
-                .withDeadlineAfter(options.getDeadline(), TimeUnit.SECONDS)
-                .query(streamQuery.build(group));
+    public StreamQueryResponse query(StreamQuery streamQuery) throws BanyanDBException {
+        checkState(this.streamServiceStub != null, "stream service is null");
+
+        final BanyandbStream.QueryResponse response = HandleExceptionsWith.callAndTranslateApiException(() ->
+                this.streamServiceBlockingStub
+                        .withDeadlineAfter(this.getOptions().getDeadline(), TimeUnit.SECONDS)
+                        .query(streamQuery.build()));
         return new StreamQueryResponse(response);
     }
 
+    /**
+     * Query measures according to given conditions
+     *
+     * @param measureQuery condition for query
+     * @return hint measures.
+     */
+    public MeasureQueryResponse query(MeasureQuery measureQuery) throws BanyanDBException {
+        checkState(this.streamServiceStub != null, "measure service is null");
+
+        final BanyandbMeasure.QueryResponse response = HandleExceptionsWith.callAndTranslateApiException(() ->
+                this.measureServiceBlockingStub
+                        .withDeadlineAfter(this.getOptions().getDeadline(), TimeUnit.SECONDS)
+                        .query(measureQuery.build()));
+        return new MeasureQueryResponse(response);
+    }
+
+    /**
+     * Define a new group and attach to the current client.
+     *
+     * @param group the group to be created
+     * @return a grouped client
+     */
+    public Group define(Group group) throws BanyanDBException {
+        GroupMetadataRegistry registry = new GroupMetadataRegistry(checkNotNull(this.channel));
+        registry.create(group);
+        return registry.get(null, group.name());
+    }
+
     /**
      * Define a new stream
      *
      * @param stream the stream to be created
-     * @return a created stream in the BanyanDB
      */
-    public Stream define(Stream stream) {
-        Preconditions.checkState(this.channel != null, "channel is null");
-        StreamMetadataRegistry registry = new StreamMetadataRegistry(this.group, this.channel);
-        registry.create(stream);
-        return registry.get(stream.getName());
+    public void define(Stream stream) throws BanyanDBException {
+        StreamMetadataRegistry streamRegistry = new StreamMetadataRegistry(checkNotNull(this.channel));
+        streamRegistry.create(stream);
+        defineIndexRules(stream, stream.indexRules());
+        MetadataCache.INSTANCE.register(stream);
     }
 
     /**
      * Define a new measure
      *
      * @param measure the measure to be created
-     * @return a created measure in the BanyanDB
      */
-    public Measure define(Measure measure) {
-        Preconditions.checkState(this.channel != null, "channel is null");
-        MeasureMetadataRegistry registry = new MeasureMetadataRegistry(this.group, this.channel);
-        registry.create(measure);
-        return registry.get(measure.getName());
+    public void define(Measure measure) throws BanyanDBException {
+        MeasureMetadataRegistry measureRegistry = new MeasureMetadataRegistry(checkNotNull(this.channel));
+        measureRegistry.create(measure);
+        defineIndexRules(measure, measure.indexRules());
+        MetadataCache.INSTANCE.register(measure);
     }
 
     /**
      * Bind index rule to the stream
      *
      * @param stream     the subject of index rule binding
-     * @param beginAt    the start timestamp of this rule binding
-     * @param expireAt   the expiry timestamp of this rule binding
      * @param indexRules rules to be bounded
      */
-    public void defineIndexRules(Stream stream, ZonedDateTime beginAt, ZonedDateTime expireAt, IndexRule... indexRules) {
+    private void defineIndexRules(Stream stream, List<IndexRule> indexRules) throws BanyanDBException {
         Preconditions.checkArgument(stream != null, "measure cannot be null");
-        Preconditions.checkState(this.channel != null, "channel is null");
-        IndexRuleMetadataRegistry irRegistry = new IndexRuleMetadataRegistry(this.group, this.channel);
-        List<String> indexRuleNames = new ArrayList<>(indexRules.length);
-        for (IndexRule ir : indexRules) {
+
+        IndexRuleMetadataRegistry irRegistry = new IndexRuleMetadataRegistry(checkNotNull(this.channel));
+        for (final IndexRule ir : indexRules) {
             irRegistry.create(ir);
-            indexRuleNames.add(ir.getName());
         }
-        IndexRuleBindingMetadataRegistry irbRegistry = new IndexRuleBindingMetadataRegistry(this.group, this.channel);
-        IndexRuleBinding binding = new IndexRuleBinding(stream.getName() + "-index-rule-binding",
-                IndexRuleBinding.Subject.referToStream(stream.getName()));
-        binding.setRules(indexRuleNames);
-        binding.setBeginAt(beginAt);
-        binding.setExpireAt(expireAt);
+
+        List<String> indexRuleNames = indexRules.stream().map(IndexRule::name).collect(Collectors.toList());
+
+        IndexRuleBindingMetadataRegistry irbRegistry = new IndexRuleBindingMetadataRegistry(checkNotNull(this.channel));
+        IndexRuleBinding binding = IndexRuleBinding.create(stream.group(),
+                IndexRuleBinding.defaultBindingRule(stream.name()),
+                IndexRuleBinding.Subject.referToStream(stream.name()),
+                indexRuleNames);
         irbRegistry.create(binding);
     }
 
@@ -282,22 +345,70 @@ public class BanyanDBClient implements Closeable {
      * @param measure    the subject of index rule binding
      * @param indexRules rules to be bounded
      */
-    public void defineIndexRules(Measure measure, IndexRule... indexRules) {
+    private void defineIndexRules(Measure measure, List<IndexRule> indexRules) throws BanyanDBException {
         Preconditions.checkArgument(measure != null, "measure cannot be null");
-        Preconditions.checkState(this.channel != null, "channel is null");
-        IndexRuleMetadataRegistry irRegistry = new IndexRuleMetadataRegistry(this.group, this.channel);
-        List<String> indexRuleNames = new ArrayList<>(indexRules.length);
-        for (IndexRule ir : indexRules) {
+
+        IndexRuleMetadataRegistry irRegistry = new IndexRuleMetadataRegistry(checkNotNull(this.channel));
+        for (final IndexRule ir : indexRules) {
             irRegistry.create(ir);
-            indexRuleNames.add(ir.getName());
         }
-        IndexRuleBindingMetadataRegistry irbRegistry = new IndexRuleBindingMetadataRegistry(this.group, this.channel);
-        IndexRuleBinding binding = new IndexRuleBinding(measure.getName() + "-index-rule-binding",
-                IndexRuleBinding.Subject.referToMeasure(measure.getName()));
-        binding.setRules(indexRuleNames);
+
+        List<String> indexRuleNames = indexRules.stream().map(IndexRule::name).collect(Collectors.toList());
+
+        IndexRuleBindingMetadataRegistry irbRegistry = new IndexRuleBindingMetadataRegistry(checkNotNull(this.channel));
+        IndexRuleBinding binding = IndexRuleBinding.create(measure.group(),
+                IndexRuleBinding.defaultBindingRule(measure.name()),
+                IndexRuleBinding.Subject.referToStream(measure.name()),
+                indexRuleNames);
         irbRegistry.create(binding);
     }
 
+    /**
+     * Try to find the stream from the BanyanDB with given group and name.
+     *
+     * @param group group of the stream
+     * @param name  name of the stream
+     * @return Steam with index rules if found. Otherwise, null is returned.
+     */
+    public Stream findStream(String group, String name) throws BanyanDBException {
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(group));
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(name));
+
+        Stream s = new StreamMetadataRegistry(checkNotNull(this.channel)).get(group, name);
+        return s.withIndexRules(findIndexRulesByGroupAndBindingName(group, IndexRuleBinding.defaultBindingRule(name)));
+    }
+
+    /**
+     * Try to find the measure from the BanyanDB with given group and name.
+     *
+     * @param group group of the measure
+     * @param name  name of the measure
+     * @return Measure with index rules if found. Otherwise, null is returned.
+     */
+    public Measure findMeasure(String group, String name) throws BanyanDBException {
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(group));
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(name));
+
+        Measure m = new MeasureMetadataRegistry(checkNotNull(this.channel)).get(group, name);
+        return m.withIndexRules(findIndexRulesByGroupAndBindingName(group, IndexRuleBinding.defaultBindingRule(name)));
+    }
+
+    private List<IndexRule> findIndexRulesByGroupAndBindingName(String group, String bindingName) throws BanyanDBException {
+        IndexRuleBindingMetadataRegistry irbRegistry = new IndexRuleBindingMetadataRegistry(checkNotNull(this.channel));
+
+        IndexRuleBinding irb = irbRegistry.get(group, bindingName);
+        if (irb == null) {
+            return Collections.emptyList();
+        }
+
+        IndexRuleMetadataRegistry irRegistry = new IndexRuleMetadataRegistry(checkNotNull(this.channel));
+        List<IndexRule> indexRules = new ArrayList<>(irb.rules().size());
+        for (final String rule : irb.rules()) {
+            indexRules.add(irRegistry.get(group, rule));
+        }
+        return indexRules;
+    }
+
     @Override
     public void close() throws IOException {
         connectionEstablishLock.lock();
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/DataPoint.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/DataPoint.java
new file mode 100644
index 0000000..634d65b
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/DataPoint.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import lombok.Getter;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * RowEntity represents an entity of BanyanDB entity.
+ */
+@Getter
+public class DataPoint extends RowEntity {
+    private final Map<String, Object> fields;
+
+    public static DataPoint create(BanyandbMeasure.DataPoint dataPoint) {
+        final DataPoint dp = new DataPoint(dataPoint);
+        dp.id = dp.getTagValue("id");
+        return dp;
+    }
+
+    private DataPoint(BanyandbMeasure.DataPoint dataPoint) {
+        super(dataPoint.getTimestamp(), dataPoint.getTagFamiliesList());
+        this.fields = new HashMap<>(dataPoint.getFieldsCount());
+        for (BanyandbMeasure.DataPoint.Field f : dataPoint.getFieldsList()) {
+            this.fields.put(f.getName(), convertToJavaType(f.getValue()));
+        }
+    }
+
+    public <T> T getFieldValue(String fieldName) {
+        return (T) this.fields.get(fieldName);
+    }
+
+    private Object convertToJavaType(BanyandbModel.FieldValue fieldValue) {
+        switch (fieldValue.getValueCase()) {
+            case INT:
+                return fieldValue.getInt().getValue();
+            case STR:
+                return fieldValue.getStr().getValue();
+            case NULL:
+                return null;
+            case BINARY_DATA:
+                return fieldValue.getBinaryData().toByteArray();
+            default:
+                throw new IllegalStateException("illegal type of FieldValue");
+        }
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureBulkWriteProcessor.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureBulkWriteProcessor.java
new file mode 100644
index 0000000..7300349
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureBulkWriteProcessor.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import io.grpc.stub.StreamObserver;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.measure.v1.MeasureServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.GRPCStreamServiceStatus;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * MeasureBulkWriteProcessor works for measure flush.
+ */
+@Slf4j
+@ThreadSafe
+public class MeasureBulkWriteProcessor extends AbstractBulkWriteProcessor<BanyandbMeasure.WriteRequest,
+        MeasureServiceGrpc.MeasureServiceStub> {
+    /**
+     * Create the processor.
+     *
+     * @param measureServiceStub stub for gRPC call.
+     * @param maxBulkSize        the max bulk size for the flush operation
+     * @param flushInterval      if given maxBulkSize is not reached in this period, the flush would be trigger
+     *                           automatically. Unit is second.
+     * @param concurrency        the number of concurrency would run for the flush max.
+     */
+    protected MeasureBulkWriteProcessor(
+            final MeasureServiceGrpc.MeasureServiceStub measureServiceStub,
+            final int maxBulkSize,
+            final int flushInterval,
+            final int concurrency) {
+        super(measureServiceStub, "MeasureBulkWriteProcessor", maxBulkSize, flushInterval, concurrency);
+    }
+
+    @Override
+    protected StreamObserver<BanyandbMeasure.WriteRequest> buildStreamObserver(MeasureServiceGrpc.MeasureServiceStub stub,
+                                                                               GRPCStreamServiceStatus status) {
+        return stub.write(new StreamObserver<BanyandbMeasure.WriteResponse>() {
+            @Override
+            public void onNext(BanyandbMeasure.WriteResponse writeResponse) {
+
+            }
+
+            @Override
+            public void onError(Throwable t) {
+                status.finished();
+                log.error("Error occurs in flushing measures", t);
+            }
+
+            @Override
+            public void onCompleted() {
+                status.finished();
+            }
+        });
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureQuery.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureQuery.java
new file mode 100644
index 0000000..4191e7a
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureQuery.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.common.base.Preconditions;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+
+import java.util.Set;
+
+/**
+ * MeasureQuery is the high-level query API for the measure model.
+ */
+@Setter
+public class MeasureQuery extends AbstractQuery<BanyandbMeasure.QueryRequest> {
+    /**
+     * field_projection can be used to select fields of the data points in the response
+     */
+    private final Set<String> fieldProjections;
+
+    private Aggregation aggregation;
+
+    public MeasureQuery(final String group, final String name, final Set<String> tagProjections, final Set<String> fieldProjections) {
+        this(group, name, null, tagProjections, fieldProjections);
+    }
+
+    public MeasureQuery(final String group, final String name, final TimestampRange timestampRange, final Set<String> tagProjections, final Set<String> fieldProjections) {
+        super(group, name, timestampRange, tagProjections);
+        this.fieldProjections = fieldProjections;
+    }
+
+    public MeasureQuery maxBy(String field, Set<String> groupByKeys) {
+        Preconditions.checkArgument(fieldProjections.contains(field), "field should be selected first");
+        Preconditions.checkArgument(this.tagProjections.containsAll(groupByKeys), "groupBy tags should be selected first");
+        this.aggregation = new Aggregation(field, Aggregation.Type.MAX, groupByKeys);
+        return this;
+    }
+
+    public MeasureQuery minBy(String field, Set<String> groupByKeys) {
+        Preconditions.checkArgument(fieldProjections.contains(field), "field should be selected first");
+        Preconditions.checkArgument(this.tagProjections.containsAll(groupByKeys), "groupBy tags should be selected first");
+        Preconditions.checkState(this.aggregation == null, "aggregation should only be set once");
+        this.aggregation = new Aggregation(field, Aggregation.Type.MIN, groupByKeys);
+        return this;
+    }
+
+    public MeasureQuery meanBy(String field, Set<String> groupByKeys) {
+        Preconditions.checkArgument(fieldProjections.contains(field), "field should be selected first");
+        Preconditions.checkArgument(this.tagProjections.containsAll(groupByKeys), "groupBy tags should be selected first");
+        Preconditions.checkState(this.aggregation == null, "aggregation should only be set once");
+        this.aggregation = new Aggregation(field, Aggregation.Type.MEAN, groupByKeys);
+        return this;
+    }
+
+    public MeasureQuery countBy(String field, Set<String> groupByKeys) {
+        Preconditions.checkArgument(fieldProjections.contains(field), "field should be selected first");
+        Preconditions.checkArgument(this.tagProjections.containsAll(groupByKeys), "groupBy tags should be selected first");
+        Preconditions.checkState(this.aggregation == null, "aggregation should only be set once");
+        this.aggregation = new Aggregation(field, Aggregation.Type.COUNT, groupByKeys);
+        return this;
+    }
+
+    public MeasureQuery sumBy(String field, Set<String> groupByKeys) {
+        Preconditions.checkArgument(fieldProjections.contains(field), "field should be selected first");
+        Preconditions.checkArgument(this.tagProjections.containsAll(groupByKeys), "groupBy tags should be selected first");
+        Preconditions.checkState(this.aggregation == null, "aggregation should only be set once");
+        this.aggregation = new Aggregation(field, Aggregation.Type.COUNT, groupByKeys);
+        return this;
+    }
+
+    /**
+     * @return QueryRequest for gRPC level query.
+     */
+    BanyandbMeasure.QueryRequest build() throws BanyanDBException {
+        final BanyandbMeasure.QueryRequest.Builder builder = BanyandbMeasure.QueryRequest.newBuilder();
+        builder.setMetadata(buildMetadata());
+        if (timestampRange != null) {
+            builder.setTimeRange(timestampRange.build());
+        }
+        builder.setTagProjection(buildTagProjections());
+        builder.setFieldProjection(BanyandbMeasure.QueryRequest.FieldProjection.newBuilder()
+                .addAllNames(fieldProjections)
+                .build());
+        if (this.aggregation != null) {
+            BanyandbMeasure.QueryRequest.GroupBy groupBy = BanyandbMeasure.QueryRequest.GroupBy.newBuilder()
+                    .setTagProjection(buildTagProjections(this.aggregation.groupByTagsProjection))
+                    .setFieldName(this.aggregation.fieldName)
+                    .build();
+            BanyandbMeasure.QueryRequest.Aggregation aggr = BanyandbMeasure.QueryRequest.Aggregation.newBuilder()
+                    .setFunction(this.aggregation.aggregationType.function)
+                    .setFieldName(this.aggregation.fieldName)
+                    .build();
+            builder.setGroupBy(groupBy).setAgg(aggr);
+        }
+        // add all criteria
+        builder.addAllCriteria(buildCriteria());
+        return builder.build();
+    }
+
+    @RequiredArgsConstructor
+    public static class Aggregation {
+        private final String fieldName;
+        private final Type aggregationType;
+        private final Set<String> groupByTagsProjection;
+
+        @RequiredArgsConstructor
+        public enum Type {
+            MEAN(BanyandbModel.AggregationFunction.AGGREGATION_FUNCTION_MEAN),
+            MAX(BanyandbModel.AggregationFunction.AGGREGATION_FUNCTION_MAX),
+            MIN(BanyandbModel.AggregationFunction.AGGREGATION_FUNCTION_MIN),
+            COUNT(BanyandbModel.AggregationFunction.AGGREGATION_FUNCTION_COUNT),
+            SUM(BanyandbModel.AggregationFunction.AGGREGATION_FUNCTION_SUM);
+            private final BanyandbModel.AggregationFunction function;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureQueryResponse.java
similarity index 63%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureQueryResponse.java
index 22c5faa..ba483d4 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureQueryResponse.java
@@ -18,29 +18,31 @@
 
 package org.apache.skywalking.banyandb.v1.client;
 
+import lombok.Getter;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import lombok.Getter;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
-
 /**
- * StreamQueryResponse represents the stream query result.
+ * MeasureQueryResponse represents the measure query result.
  */
-public class StreamQueryResponse {
+public class MeasureQueryResponse {
     @Getter
-    private final List<RowEntity> elements;
+    private final List<DataPoint> dataPoints;
 
-    StreamQueryResponse(BanyandbStream.QueryResponse response) {
-        final List<BanyandbStream.Element> elementsList = response.getElementsList();
-        elements = new ArrayList<>(elementsList.size());
-        elementsList.forEach(element -> elements.add(new RowEntity(element)));
+    MeasureQueryResponse(BanyandbMeasure.QueryResponse response) {
+        final List<BanyandbMeasure.DataPoint> dataPointList = response.getDataPointsList();
+        this.dataPoints = new ArrayList<>(dataPointList.size());
+        for (final BanyandbMeasure.DataPoint dp : dataPointList) {
+            dataPoints.add(DataPoint.create(dp));
+        }
     }
 
     /**
      * @return size of the response set.
      */
     public int size() {
-        return elements.size();
+        return dataPoints.size();
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureWrite.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureWrite.java
new file mode 100644
index 0000000..99cdae3
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/MeasureWrite.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.protobuf.Timestamp;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Serializable;
+
+import java.util.Deque;
+import java.util.LinkedList;
+
+public class MeasureWrite extends AbstractWrite<BanyandbMeasure.WriteRequest> {
+    /**
+     * The values of "searchable" fields, which are defined by the schema.
+     * In the bulk write process, BanyanDB client doesn't require field names anymore.
+     */
+    private final Object[] fields;
+
+    public MeasureWrite(final String group, final String name, long timestamp) {
+        super(group, name, timestamp);
+        this.fields = new Object[this.entityMetadata.getTotalFields()];
+    }
+
+    public MeasureWrite field(String fieldName, Serializable<BanyandbModel.FieldValue> fieldVal) {
+        final int index = this.entityMetadata.findFieldInfo(fieldName);
+        this.fields[index] = fieldVal;
+        return this;
+    }
+
+    @Override
+    public MeasureWrite tag(String tagName, Serializable<BanyandbModel.TagValue> tagValue) throws BanyanDBException {
+        return (MeasureWrite) super.tag(tagName, tagValue);
+    }
+
+    /**
+     * Build a write request
+     *
+     * @return {@link BanyandbMeasure.WriteRequest} for the bulk process.
+     */
+    @Override
+    protected BanyandbMeasure.WriteRequest build(BanyandbCommon.Metadata metadata, Timestamp ts) {
+        final BanyandbMeasure.WriteRequest.Builder builder = BanyandbMeasure.WriteRequest.newBuilder();
+        builder.setMetadata(metadata);
+        final BanyandbMeasure.DataPointValue.Builder datapointValueBuilder = BanyandbMeasure.DataPointValue.newBuilder();
+        datapointValueBuilder.setTimestamp(ts);
+        // memorize the last offset for the last tag family
+        int lastFamilyOffset = 0;
+        for (final int tagsPerFamily : this.entityMetadata.getTagFamilyCapacity()) {
+            final BanyandbModel.TagFamilyForWrite.Builder b = BanyandbModel.TagFamilyForWrite.newBuilder();
+            boolean firstNonNullTagFound = false;
+            Deque<BanyandbModel.TagValue> tags = new LinkedList<>();
+            for (int j = tagsPerFamily - 1; j >= 0; j--) {
+                Object obj = this.tags[lastFamilyOffset + j];
+                if (obj == null) {
+                    if (firstNonNullTagFound) {
+                        b.addTags(TagAndValue.nullTagValue().serialize());
+                    }
+                    continue;
+                }
+                firstNonNullTagFound = true;
+                tags.addFirst(((Serializable<BanyandbModel.TagValue>) obj).serialize());
+            }
+            lastFamilyOffset += tagsPerFamily;
+            datapointValueBuilder.addTagFamilies(b.addAllTags(tags).build());
+        }
+
+        for (int i = 0; i < this.entityMetadata.getTotalFields(); i++) {
+            Object obj = this.fields[i];
+            if (obj != null) {
+                datapointValueBuilder.addFields(((Serializable<BanyandbModel.FieldValue>) obj).serialize());
+            } else {
+                datapointValueBuilder.addFields(TagAndValue.nullFieldValue().serialize());
+            }
+        }
+
+        builder.setDataPoint(datapointValueBuilder);
+        return builder.build();
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
index 89b8038..109fd19 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
@@ -21,12 +21,13 @@ package org.apache.skywalking.banyandb.v1.client;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Setter;
+import org.apache.skywalking.banyandb.v1.client.grpc.channel.ChannelManagerSettings;
 
 /**
  * Client connection options.
  */
 @Setter
-@Getter(AccessLevel.PACKAGE)
+@Getter(AccessLevel.PUBLIC)
 public class Options {
     /**
      * Max inbound message size
@@ -36,8 +37,39 @@ public class Options {
      * Threshold of gRPC blocking query, unit is second
      */
     private int deadline = 30;
+    /**
+     * Refresh interval for the gRPC channel, unit is second
+     */
+    private long refreshInterval = 30;
+    /**
+     * Threshold of force gRPC reconnection if network issue is encountered
+     */
+    private long forceReconnectionThreshold = 1;
+    /**
+     * Force use TLS for gRPC
+     * Default is false
+     */
+    private boolean forceTLS = false;
+    /**
+     * SSL: Trusted CA Path
+     */
+    private String sslTrustCAPath = "";
+    /**
+     * SSL: Cert Chain Path
+     */
+    private String sslCertChainPath = "";
+    /**
+     * SSL: Cert Key Path
+     */
+    private String sslKeyPath = "";
 
     Options() {
     }
 
+    ChannelManagerSettings buildChannelManagerSettings() {
+        return ChannelManagerSettings.newBuilder()
+                .setRefreshInterval(this.refreshInterval)
+                .setForceReconnectionThreshold(this.forceReconnectionThreshold)
+                .build();
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/PairQueryCondition.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/PairQueryCondition.java
index ee3a81a..ff7540f 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/PairQueryCondition.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/PairQueryCondition.java
@@ -18,7 +18,7 @@
 
 package org.apache.skywalking.banyandb.v1.client;
 
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
 
 import java.util.List;
 
@@ -26,15 +26,15 @@ import java.util.List;
  * PairQuery represents a query condition, including tag name, operator, and value(s);
  */
 public abstract class PairQueryCondition<T> extends TagAndValue<T> {
-    protected final Banyandb.Condition.BinaryOp op;
+    protected final BanyandbModel.Condition.BinaryOp op;
 
-    private PairQueryCondition(String tagFamilyName, String tagName, Banyandb.Condition.BinaryOp op, T value) {
-        super(tagFamilyName, tagName, value);
+    private PairQueryCondition(String tagName, BanyandbModel.Condition.BinaryOp op, T value) {
+        super(tagName, value);
         this.op = op;
     }
 
-    Banyandb.Condition build() {
-        return Banyandb.Condition.newBuilder()
+    BanyandbModel.Condition build() {
+        return BanyandbModel.Condition.newBuilder()
                 .setName(this.tagName)
                 .setOp(this.op)
                 .setValue(buildTypedPair()).build();
@@ -45,20 +45,20 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
      *
      * @return typedPair to be included
      */
-    protected abstract Banyandb.TagValue buildTypedPair();
+    protected abstract BanyandbModel.TagValue buildTypedPair();
 
     /**
      * LongQueryCondition represents `tag(Long) $op value` condition.
      */
     public static class LongQueryCondition extends PairQueryCondition<Long> {
-        private LongQueryCondition(String tagFamilyName, String tagName, Banyandb.Condition.BinaryOp op, Long value) {
-            super(tagFamilyName, tagName, op, value);
+        private LongQueryCondition(String tagName, BanyandbModel.Condition.BinaryOp op, Long value) {
+            super(tagName, op, value);
         }
 
         @Override
-        protected Banyandb.TagValue buildTypedPair() {
-            return Banyandb.TagValue.newBuilder()
-                    .setInt(Banyandb.Int
+        protected BanyandbModel.TagValue buildTypedPair() {
+            return BanyandbModel.TagValue.newBuilder()
+                    .setInt(BanyandbModel.Int
                             .newBuilder()
                             .setValue(value).build())
                     .build();
@@ -66,80 +66,74 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
 
         /**
          * Build a query condition for {@link Long} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_EQ} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_EQ} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `Long == value`
          */
-        public static PairQueryCondition<Long> eq(String tagFamilyName, String tagName, Long val) {
-            return new LongQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_EQ, val);
+        public static PairQueryCondition<Long> eq(String tagName, Long val) {
+            return new LongQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_EQ, val);
         }
 
         /**
          * Build a query condition for {@link Long} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_NE} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_NE} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `Long != value`
          */
-        public static PairQueryCondition<Long> ne(String tagFamilyName, String tagName, Long val) {
-            return new LongQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_NE, val);
+        public static PairQueryCondition<Long> ne(String tagName, Long val) {
+            return new LongQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_NE, val);
         }
 
         /**
          * Build a query condition for {@link Long} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_GT} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_GT} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `Long &gt; value`
          */
-        public static PairQueryCondition<Long> gt(String tagFamilyName, String tagName, Long val) {
-            return new LongQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_GT, val);
+        public static PairQueryCondition<Long> gt(String tagName, Long val) {
+            return new LongQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_GT, val);
         }
 
         /**
          * Build a query condition for {@link Long} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_GE} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_GE} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `Long &ge; value`
          */
-        public static PairQueryCondition<Long> ge(String tagFamilyName, String tagName, Long val) {
-            return new LongQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_GE, val);
+        public static PairQueryCondition<Long> ge(String tagName, Long val) {
+            return new LongQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_GE, val);
         }
 
         /**
          * Build a query condition for {@link Long} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_LT} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_LT} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `Long &lt; value`
          */
-        public static PairQueryCondition<Long> lt(String tagFamilyName, String tagName, Long val) {
-            return new LongQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_LT, val);
+        public static PairQueryCondition<Long> lt(String tagName, Long val) {
+            return new LongQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_LT, val);
         }
 
         /**
          * Build a query condition for {@link Long} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_LE} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_LE} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `Long &le; value`
          */
-        public static PairQueryCondition<Long> le(String tagFamilyName, String tagName, Long val) {
-            return new LongQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_LE, val);
+        public static PairQueryCondition<Long> le(String tagName, Long val) {
+            return new LongQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_LE, val);
         }
     }
 
@@ -147,14 +141,14 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
      * StringQueryCondition represents `tag(String) $op value` condition.
      */
     public static class StringQueryCondition extends PairQueryCondition<String> {
-        private StringQueryCondition(String tagFamilyName, String tagName, Banyandb.Condition.BinaryOp op, String value) {
-            super(tagFamilyName, tagName, op, value);
+        private StringQueryCondition(String tagName, BanyandbModel.Condition.BinaryOp op, String value) {
+            super(tagName, op, value);
         }
 
         @Override
-        protected Banyandb.TagValue buildTypedPair() {
-            return Banyandb.TagValue.newBuilder()
-                    .setStr(Banyandb.Str
+        protected BanyandbModel.TagValue buildTypedPair() {
+            return BanyandbModel.TagValue.newBuilder()
+                    .setStr(BanyandbModel.Str
                             .newBuilder()
                             .setValue(value).build())
                     .build();
@@ -162,28 +156,26 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
 
         /**
          * Build a query condition for {@link String} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_EQ} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_EQ} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `String == value`
          */
-        public static PairQueryCondition<String> eq(String tagFamilyName, String tagName, String val) {
-            return new StringQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_EQ, val);
+        public static PairQueryCondition<String> eq(String tagName, String val) {
+            return new StringQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_EQ, val);
         }
 
         /**
          * Build a query condition for {@link String} type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_NE} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_NE} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `String != value`
          */
-        public static PairQueryCondition<String> ne(String tagFamilyName, String tagName, String val) {
-            return new StringQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_NE, val);
+        public static PairQueryCondition<String> ne(String tagName, String val) {
+            return new StringQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_NE, val);
         }
     }
 
@@ -191,14 +183,14 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
      * StringArrayQueryCondition represents `tag(List of String) $op value` condition.
      */
     public static class StringArrayQueryCondition extends PairQueryCondition<List<String>> {
-        private StringArrayQueryCondition(String tagFamilyName, String tagName, Banyandb.Condition.BinaryOp op, List<String> value) {
-            super(tagFamilyName, tagName, op, value);
+        private StringArrayQueryCondition(String tagName, BanyandbModel.Condition.BinaryOp op, List<String> value) {
+            super(tagName, op, value);
         }
 
         @Override
-        protected Banyandb.TagValue buildTypedPair() {
-            return Banyandb.TagValue.newBuilder()
-                    .setStrArray(Banyandb.StrArray
+        protected BanyandbModel.TagValue buildTypedPair() {
+            return BanyandbModel.TagValue.newBuilder()
+                    .setStrArray(BanyandbModel.StrArray
                             .newBuilder()
                             .addAllValue(value).build())
                     .build();
@@ -206,54 +198,50 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
 
         /**
          * Build a query condition for {@link List} of {@link String} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_EQ} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_EQ} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[String] == values`
          */
-        public static PairQueryCondition<List<String>> eq(String tagFamilyName, String tagName, List<String> val) {
-            return new StringArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_EQ, val);
+        public static PairQueryCondition<List<String>> eq(String tagName, List<String> val) {
+            return new StringArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_EQ, val);
         }
 
         /**
          * Build a query condition for {@link List} of {@link String} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_NE} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_NE} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[String] != values`
          */
-        public static PairQueryCondition<List<String>> ne(String tagFamilyName, String tagName, List<String> val) {
-            return new StringArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_NE, val);
+        public static PairQueryCondition<List<String>> ne(String tagName, List<String> val) {
+            return new StringArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_NE, val);
         }
 
         /**
          * Build a query condition for {@link List} of {@link String} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_HAVING} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_HAVING} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[String] having values`
          */
-        public static PairQueryCondition<List<String>> having(String tagFamilyName, String tagName, List<String> val) {
-            return new StringArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_HAVING, val);
+        public static PairQueryCondition<List<String>> having(String tagName, List<String> val) {
+            return new StringArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_HAVING, val);
         }
 
         /**
          * Build a query condition for {@link List} of {@link String} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_NOT_HAVING} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_NOT_HAVING} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[String] not having values`
          */
-        public static PairQueryCondition<List<String>> notHaving(String tagFamilyName, String tagName, List<String> val) {
-            return new StringArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_NOT_HAVING, val);
+        public static PairQueryCondition<List<String>> notHaving(String tagName, List<String> val) {
+            return new StringArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_NOT_HAVING, val);
         }
     }
 
@@ -261,14 +249,14 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
      * LongArrayQueryCondition represents `tag(List of Long) $op value` condition.
      */
     public static class LongArrayQueryCondition extends PairQueryCondition<List<Long>> {
-        private LongArrayQueryCondition(String tagFamilyName, String tagName, Banyandb.Condition.BinaryOp op, List<Long> value) {
-            super(tagFamilyName, tagName, op, value);
+        private LongArrayQueryCondition(String tagName, BanyandbModel.Condition.BinaryOp op, List<Long> value) {
+            super(tagName, op, value);
         }
 
         @Override
-        protected Banyandb.TagValue buildTypedPair() {
-            return Banyandb.TagValue.newBuilder()
-                    .setIntArray(Banyandb.IntArray
+        protected BanyandbModel.TagValue buildTypedPair() {
+            return BanyandbModel.TagValue.newBuilder()
+                    .setIntArray(BanyandbModel.IntArray
                             .newBuilder()
                             .addAllValue(value).build())
                     .build();
@@ -276,54 +264,51 @@ public abstract class PairQueryCondition<T> extends TagAndValue<T> {
 
         /**
          * Build a query condition for {@link List} of {@link Long} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_EQ} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_EQ} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[Long] == value`
          */
-        public static PairQueryCondition<List<Long>> eq(String tagFamilyName, String tagName, List<Long> val) {
-            return new LongArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_EQ, val);
+        public static PairQueryCondition<List<Long>> eq(String tagName, List<Long> val) {
+            return new LongArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_EQ, val);
         }
 
         /**
          * Build a query condition for {@link List} of {@link Long} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_NE} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_NE} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[Long] != value`
          */
-        public static PairQueryCondition<List<Long>> ne(String tagFamilyName, String tagName, List<Long> val) {
-            return new LongArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_NE, val);
+        public static PairQueryCondition<List<Long>> ne(String tagName, List<Long> val) {
+            return new LongArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_NE, val);
         }
 
         /**
          * Build a query condition for {@link List} of {@link Long} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_HAVING} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_HAVING} as the relation
          *
-         * @param tagFamilyName family name of the tag
          * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param val     value of the tag
          * @return a query that `[Long] having values`
          */
-        public static PairQueryCondition<List<Long>> having(String tagFamilyName, String tagName, List<Long> val) {
-            return new LongArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_HAVING, val);
+        public static PairQueryCondition<List<Long>> having(String tagName, List<Long> val) {
+            return new LongArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_HAVING, val);
         }
 
         /**
          * Build a query condition for {@link List} of {@link Long} as the type
-         * and {@link Banyandb.Condition.BinaryOp#BINARY_OP_NOT_HAVING} as the relation
+         * and {@link BanyandbModel.Condition.BinaryOp#BINARY_OP_NOT_HAVING} as the relation
          *
          * @param tagFamilyName family name of the tag
-         * @param tagName name of the tag
-         * @param val       value of the tag
+         * @param tagName       name of the tag
+         * @param val           value of the tag
          * @return a query that `[Long] not having values`
          */
-        public static PairQueryCondition<List<Long>> notHaving(String tagFamilyName, String tagName, List<Long> val) {
-            return new LongArrayQueryCondition(tagFamilyName, tagName, Banyandb.Condition.BinaryOp.BINARY_OP_NOT_HAVING, val);
+        public static PairQueryCondition<List<Long>> notHaving(String tagName, List<Long> val) {
+            return new LongArrayQueryCondition(tagName, BanyandbModel.Condition.BinaryOp.BINARY_OP_NOT_HAVING, val);
         }
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/RowEntity.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/RowEntity.java
index 4b839a3..e546956 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/RowEntity.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/RowEntity.java
@@ -18,14 +18,14 @@
 
 package org.apache.skywalking.banyandb.v1.client;
 
-import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import java.util.Map;
 
+import com.google.protobuf.Timestamp;
 import lombok.Getter;
-import org.apache.skywalking.banyandb.v1.Banyandb;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
 
 /**
  * RowEntity represents an entity of BanyanDB entity.
@@ -36,29 +36,61 @@ public class RowEntity {
      * identity of the entity.
      * For a trace entity, it is the spanID of a Span or the segmentId of a segment in Skywalking.
      */
-    private final String id;
+    protected String id;
 
     /**
      * timestamp of the entity in the timeunit of milliseconds.
      */
-    private final long timestamp;
+    protected final long timestamp;
 
     /**
-     * fields are indexed-fields that are searchable in BanyanBD
+     * tags is a map maintaining the relation between tag name and its value,
+     * (in the format of Java Types converted from gRPC Types).
+     * The family name is thus ignored, since the name should be globally unique for a schema.
      */
-    private final List<List<TagAndValue<?>>> tagFamilies;
+    protected final Map<String, Object> tags;
 
-    RowEntity(BanyandbStream.Element element) {
-        id = element.getElementId();
-        timestamp = element.getTimestamp().getSeconds() * 1000 + element.getTimestamp().getNanos() / 1_000_000;
-        final int tagFamilyCount = element.getTagFamiliesCount();
-        this.tagFamilies = new ArrayList<>(tagFamilyCount);
-        for (int i = 0; i < tagFamilyCount; i++) {
-            Banyandb.TagFamily tagFamily = element.getTagFamilies(i);
-            List<TagAndValue<?>> tagAndValuesInTagFamily = tagFamily.getTagsList().stream()
-                    .map((Function<Banyandb.Tag, TagAndValue<?>>) tag -> TagAndValue.build(tagFamily.getName(), tag))
-                    .collect(Collectors.toList());
-            this.tagFamilies.add(tagAndValuesInTagFamily);
+    public static RowEntity create(BanyandbStream.Element element) {
+        final RowEntity rowEntity = new RowEntity(element.getTimestamp(), element.getTagFamiliesList());
+        rowEntity.id = element.getElementId();
+        return rowEntity;
+    }
+
+    protected RowEntity(Timestamp ts, List<BanyandbModel.TagFamily> tagFamilyList) {
+        timestamp = ts.getSeconds() * 1000 + ts.getNanos() / 1_000_000;
+        this.tags = new HashMap<>();
+        for (final BanyandbModel.TagFamily tagFamily : tagFamilyList) {
+            for (final BanyandbModel.Tag tag : tagFamily.getTagsList()) {
+                final Object val = convertToJavaType(tag.getValue());
+                if (val != null) {
+                    this.tags.put(tag.getKey(), val);
+                }
+            }
+        }
+    }
+
+    public <T> T getTagValue(String tagName) {
+        return (T) this.tags.get(tagName);
+    }
+
+    private Object convertToJavaType(BanyandbModel.TagValue tagValue) {
+        switch (tagValue.getValueCase()) {
+            case INT:
+                return tagValue.getInt().getValue();
+            case STR:
+                return tagValue.getStr().getValue();
+            case NULL:
+                return null;
+            case INT_ARRAY:
+                return tagValue.getIntArray().getValueList();
+            case STR_ARRAY:
+                return tagValue.getStrArray().getValueList();
+            case BINARY_DATA:
+                return tagValue.getBinaryData().toByteArray();
+            case ID:
+                return tagValue.getId().getValue();
+            default:
+                throw new IllegalStateException("illegal type of TagValue");
         }
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamBulkWriteProcessor.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamBulkWriteProcessor.java
index cdf7771..e5c94ea 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamBulkWriteProcessor.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamBulkWriteProcessor.java
@@ -20,12 +20,10 @@ package org.apache.skywalking.banyandb.v1.client;
 
 import io.grpc.stub.StreamObserver;
 
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
 import lombok.extern.slf4j.Slf4j;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
-import org.apache.skywalking.banyandb.v1.stream.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.stream.v1.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
+import org.apache.skywalking.banyandb.v1.client.grpc.GRPCStreamServiceStatus;
 
 import javax.annotation.concurrent.ThreadSafe;
 
@@ -34,74 +32,43 @@ import javax.annotation.concurrent.ThreadSafe;
  */
 @Slf4j
 @ThreadSafe
-public class StreamBulkWriteProcessor extends BulkWriteProcessor {
-    /**
-     * The BanyanDB instance name.
-     */
-    private final String group;
-    private StreamServiceGrpc.StreamServiceStub streamServiceStub;
-
+public class StreamBulkWriteProcessor extends AbstractBulkWriteProcessor<BanyandbStream.WriteRequest,
+        StreamServiceGrpc.StreamServiceStub> {
     /**
      * Create the processor.
      *
-     * @param streamServiceStub stub for gRPC call.
-     * @param maxBulkSize       the max bulk size for the flush operation
-     * @param flushInterval     if given maxBulkSize is not reached in this period, the flush would be trigger
-     *                          automatically. Unit is second.
-     * @param concurrency       the number of concurrency would run for the flush max.
-     */
-    protected StreamBulkWriteProcessor(final String group,
-                                       final StreamServiceGrpc.StreamServiceStub streamServiceStub,
-                                       final int maxBulkSize,
-                                       final int flushInterval,
-                                       final int concurrency) {
-        super("StreamBulkWriteProcessor", maxBulkSize, flushInterval, concurrency);
-        this.group = group;
-        this.streamServiceStub = streamServiceStub;
-    }
-
-    /**
-     * Add the stream to the bulk processor.
-     *
-     * @param streamWrite to add.
+     * @param serviceStub   stub for gRPC call.
+     * @param maxBulkSize   the max bulk size for the flush operation
+     * @param flushInterval if given maxBulkSize is not reached in this period, the flush would be trigger
+     *                      automatically. Unit is second.
+     * @param concurrency   the number of concurrency would run for the flush max.
      */
-    public void add(StreamWrite streamWrite) {
-        this.buffer.produce(streamWrite);
+    protected StreamBulkWriteProcessor(
+            final StreamServiceGrpc.StreamServiceStub serviceStub,
+            final int maxBulkSize,
+            final int flushInterval,
+            final int concurrency) {
+        super(serviceStub, "StreamBulkWriteProcessor", maxBulkSize, flushInterval, concurrency);
     }
 
     @Override
-    protected void flush(final List data) {
-        final StreamObserver<BanyandbStream.WriteRequest> writeRequestStreamObserver
-                = streamServiceStub.withDeadlineAfter(
-                        flushInterval, TimeUnit.SECONDS)
-                .write(
-                        new StreamObserver<BanyandbStream.WriteResponse>() {
-                            @Override
-                            public void onNext(
-                                    BanyandbStream.WriteResponse writeResponse) {
-                            }
+    protected StreamObserver<BanyandbStream.WriteRequest> buildStreamObserver(StreamServiceGrpc.StreamServiceStub stub, GRPCStreamServiceStatus status) {
+        return stub.write(
+                new StreamObserver<BanyandbStream.WriteResponse>() {
+                    @Override
+                    public void onNext(BanyandbStream.WriteResponse writeResponse) {
+                    }
 
-                            @Override
-                            public void onError(
-                                    Throwable throwable) {
-                                log.error(
-                                        "Error occurs in flushing streams.",
-                                        throwable
-                                );
-                            }
+                    @Override
+                    public void onError(Throwable t) {
+                        status.finished();
+                        log.error("Error occurs in flushing streams", t);
+                    }
 
-                            @Override
-                            public void onCompleted() {
-                            }
-                        });
-        try {
-            data.forEach(write -> {
-                final StreamWrite streamWrite = (StreamWrite) write;
-                BanyandbStream.WriteRequest request = streamWrite.build(group);
-                writeRequestStreamObserver.onNext(request);
-            });
-        } finally {
-            writeRequestStreamObserver.onCompleted();
-        }
+                    @Override
+                    public void onCompleted() {
+                        status.finished();
+                    }
+                });
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQuery.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQuery.java
index f4d260e..9be86a9 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQuery.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQuery.java
@@ -18,39 +18,19 @@
 
 package org.apache.skywalking.banyandb.v1.client;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.Set;
 
 import lombok.RequiredArgsConstructor;
 import lombok.Setter;
-import org.apache.skywalking.banyandb.v1.Banyandb;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
 
 /**
  * StreamQuery is the high-level query API for the stream model.
  */
 @Setter
-public class StreamQuery {
-    /**
-     * Owner name current entity
-     */
-    private final String name;
-    /**
-     * The time range for query.
-     */
-    private final TimestampRange timestampRange;
-    /**
-     * The projections of query result.
-     * These should have defined in the schema and must be `searchable`.
-     */
-    private final List<String> projections;
-    /**
-     * Query conditions.
-     */
-    private final List<PairQueryCondition<?>> conditions;
+public class StreamQuery extends AbstractQuery<BanyandbStream.QueryRequest> {
     /**
      * The starting row id of the query. Default value is 0.
      */
@@ -63,74 +43,32 @@ public class StreamQuery {
      * One order condition is supported and optional.
      */
     private OrderBy orderBy;
-    /**
-     * Whether to fetch unindexed fields from the "data" tag family for the query
-     */
-    private List<String> dataProjections;
 
-    public StreamQuery(final String name, final TimestampRange timestampRange, final List<String> projections) {
-        this.name = name;
-        this.timestampRange = timestampRange;
-        this.projections = projections;
-        this.conditions = new ArrayList<>(10);
+    public StreamQuery(final String group, final String name, final TimestampRange timestampRange, final Set<String> projections) {
+        super(group, name, timestampRange, projections);
         this.offset = 0;
         this.limit = 20;
-        // by default, we don't need projection for data tag family
-        this.dataProjections = Collections.emptyList();
     }
 
-    public StreamQuery(final String name, final List<String> projections) {
-        this(name, null, projections);
+    public StreamQuery(final String group, final String name, final Set<String> projections) {
+        this(group, name, null, projections);
     }
 
-    /**
-     * Fluent API for appending query condition
-     *
-     * @param condition the query condition to be appended
-     */
+    @Override
     public StreamQuery appendCondition(PairQueryCondition<?> condition) {
-        this.conditions.add(condition);
-        return this;
+        return (StreamQuery) super.appendCondition(condition);
     }
 
-    /**
-     * @param group The instance name.
-     * @return QueryRequest for gRPC level query.
-     */
-    BanyandbStream.QueryRequest build(String group) {
-        final BanyandbStream.QueryRequest.Builder builder = BanyandbStream.QueryRequest.newBuilder();
-        builder.setMetadata(Banyandb.Metadata.newBuilder()
-                .setGroup(group)
-                .setName(name)
-                .build());
+    @Override
+    BanyandbStream.QueryRequest build() throws BanyanDBException {
+        final BanyandbStream.QueryRequest.Builder builder = BanyandbStream.QueryRequest.newBuilder()
+                .setMetadata(buildMetadata());
         if (timestampRange != null) {
             builder.setTimeRange(timestampRange.build());
         }
-        // set projection
-        Banyandb.Projection.Builder projectionBuilder = Banyandb.Projection.newBuilder()
-                .addTagFamilies(Banyandb.Projection.TagFamily.newBuilder()
-                        .setName("searchable")
-                        .addAllTags(this.projections)
-                        .build());
-        if (!this.dataProjections.isEmpty()) {
-            projectionBuilder.addTagFamilies(Banyandb.Projection.TagFamily.newBuilder()
-                    .setName("data")
-                    .addAllTags(this.dataProjections)
-                    .build());
-        }
-        builder.setProjection(projectionBuilder);
+        builder.setProjection(buildTagProjections());
         // set conditions grouped by tagFamilyName
-        Map<String, List<PairQueryCondition<?>>> groupedConditions = conditions.stream()
-                .collect(Collectors.groupingBy(TagAndValue::getTagFamilyName));
-        for (final Map.Entry<String, List<PairQueryCondition<?>>> tagFamily : groupedConditions.entrySet()) {
-            final List<Banyandb.Condition> conditionList = tagFamily.getValue().stream().map(PairQueryCondition::build)
-                    .collect(Collectors.toList());
-            BanyandbStream.QueryRequest.Criteria criteria = BanyandbStream.QueryRequest.Criteria
-                    .newBuilder()
-                    .setTagFamilyName(tagFamily.getKey())
-                    .addAllConditions(conditionList).build();
-            builder.addCriteria(criteria);
-        }
+        builder.addAllCriteria(buildCriteria());
         builder.setOffset(offset);
         builder.setLimit(limit);
         if (orderBy != null) {
@@ -150,11 +88,11 @@ public class StreamQuery {
          */
         private final Type type;
 
-        private Banyandb.QueryOrder build() {
-            final Banyandb.QueryOrder.Builder builder = Banyandb.QueryOrder.newBuilder();
+        private BanyandbModel.QueryOrder build() {
+            final BanyandbModel.QueryOrder.Builder builder = BanyandbModel.QueryOrder.newBuilder();
             builder.setIndexRuleName(indexRuleName);
             builder.setSort(
-                    Type.DESC.equals(type) ? Banyandb.QueryOrder.Sort.SORT_DESC : Banyandb.QueryOrder.Sort.SORT_ASC);
+                    Type.DESC.equals(type) ? BanyandbModel.Sort.SORT_DESC : BanyandbModel.Sort.SORT_ASC);
             return builder.build();
         }
 
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
index 22c5faa..baa06e5 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
@@ -22,7 +22,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import lombok.Getter;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
 
 /**
  * StreamQueryResponse represents the stream query result.
@@ -34,7 +34,7 @@ public class StreamQueryResponse {
     StreamQueryResponse(BanyandbStream.QueryResponse response) {
         final List<BanyandbStream.Element> elementsList = response.getElementsList();
         elements = new ArrayList<>(elementsList.size());
-        elementsList.forEach(element -> elements.add(new RowEntity(element)));
+        elementsList.forEach(element -> elements.add(RowEntity.create(element)));
     }
 
     /**
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamWrite.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamWrite.java
index 92eb7a0..b055d17 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamWrite.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamWrite.java
@@ -20,74 +20,69 @@ package org.apache.skywalking.banyandb.v1.client;
 
 import com.google.protobuf.Timestamp;
 
-import java.util.List;
-
-import lombok.AccessLevel;
-import lombok.Builder;
 import lombok.Getter;
-import lombok.Singular;
-import org.apache.skywalking.banyandb.v1.Banyandb;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Serializable;
+
+import java.util.Deque;
+import java.util.LinkedList;
 
 /**
  * StreamWrite represents a write operation, including necessary fields, for {@link
  * BanyanDBClient#buildStreamWriteProcessor}.
  */
-@Builder
-@Getter(AccessLevel.PROTECTED)
-public class StreamWrite {
-    /**
-     * Owner name current entity
-     */
-    private final String name;
+public class StreamWrite extends AbstractWrite<BanyandbStream.WriteRequest> {
     /**
      * ID of current entity
      */
+    @Getter
     private final String elementId;
-    /**
-     * Timestamp represents the time of current stream
-     * in the timeunit of milliseconds.
-     */
-    private final long timestamp;
-    /**
-     * The fields represent objects of current stream, and they are not indexed.
-     * It could be organized by different serialization formats.
-     * For instance, regarding the binary format, SkyWalking may use protobuf, but it is not required.
-     * The BanyanDB server wouldn't deserialize binary data. Thus, no specific format requirement.
-     */
-    @Singular
-    private final List<SerializableTag<Banyandb.TagValue>> dataTags;
-    /**
-     * The values of "searchable" fields, which are defined by the schema.
-     * In the bulk write process, BanyanDB client doesn't require field names anymore.
-     */
-    @Singular
-    private final List<SerializableTag<Banyandb.TagValue>> searchableTags;
+
+    public StreamWrite(final String group, final String name, final String elementId, long timestamp) {
+        super(group, name, timestamp);
+        this.elementId = elementId;
+    }
+
+    @Override
+    public StreamWrite tag(String tagName, Serializable<BanyandbModel.TagValue> tagValue) throws BanyanDBException {
+        return (StreamWrite) super.tag(tagName, tagValue);
+    }
 
     /**
-     * @param group of the BanyanDB client connected.
+     * Build a write request
+     *
      * @return {@link BanyandbStream.WriteRequest} for the bulk process.
      */
-    BanyandbStream.WriteRequest build(String group) {
+    @Override
+    protected BanyandbStream.WriteRequest build(BanyandbCommon.Metadata metadata, Timestamp ts) {
         final BanyandbStream.WriteRequest.Builder builder = BanyandbStream.WriteRequest.newBuilder();
-        builder.setMetadata(Banyandb.Metadata.newBuilder().setGroup(group).setName(name).build());
+        builder.setMetadata(metadata);
         final BanyandbStream.ElementValue.Builder elemValBuilder = BanyandbStream.ElementValue.newBuilder();
         elemValBuilder.setElementId(elementId);
-        elemValBuilder.setTimestamp(Timestamp.newBuilder()
-                .setSeconds(timestamp / 1000)
-                .setNanos((int) (timestamp % 1000 * 1_000_000)));
-        // 1 - add "data" tags
-        Banyandb.TagFamilyForWrite.Builder dataBuilder = Banyandb.TagFamilyForWrite.newBuilder();
-        for (final SerializableTag<Banyandb.TagValue> dataTag : this.dataTags) {
-            dataBuilder.addTags(dataTag.toTag());
-        }
-        elemValBuilder.addTagFamilies(dataBuilder.build());
-        // 2 - add "searchable" tags
-        Banyandb.TagFamilyForWrite.Builder searchableBuilder = Banyandb.TagFamilyForWrite.newBuilder();
-        for (final SerializableTag<Banyandb.TagValue> searchableTag : this.searchableTags) {
-            searchableBuilder.addTags(searchableTag.toTag());
+        elemValBuilder.setTimestamp(ts);
+        // memorize the last offset for the last tag family
+        int lastFamilyOffset = 0;
+        for (final int tagsPerFamily : this.entityMetadata.getTagFamilyCapacity()) {
+            final BanyandbModel.TagFamilyForWrite.Builder b = BanyandbModel.TagFamilyForWrite.newBuilder();
+            boolean firstNonNullTagFound = false;
+            Deque<BanyandbModel.TagValue> tags = new LinkedList<>();
+            for (int j = tagsPerFamily - 1; j >= 0; j--) {
+                Object obj = this.tags[lastFamilyOffset + j];
+                if (obj == null) {
+                    if (firstNonNullTagFound) {
+                        b.addTags(TagAndValue.nullTagValue().serialize());
+                    }
+                    continue;
+                }
+                firstNonNullTagFound = true;
+                tags.addFirst(((Serializable<BanyandbModel.TagValue>) obj).serialize());
+            }
+            lastFamilyOffset += tagsPerFamily;
+            elemValBuilder.addTagFamilies(b.addAllTags(tags).build());
         }
-        elemValBuilder.addTagFamilies(searchableBuilder);
         builder.setElement(elemValBuilder);
         return builder.build();
     }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Tag.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/Tag.java
deleted file mode 100644
index 8db3f4e..0000000
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Tag.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.apache.skywalking.banyandb.v1.client;
-
-import java.util.List;
-
-import com.google.protobuf.ByteString;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import org.apache.skywalking.banyandb.v1.Banyandb;
-
-import static com.google.protobuf.NullValue.NULL_VALUE;
-
-/**
- * Field represents a value of column/field in the write-op or response.
- */
-@EqualsAndHashCode
-public abstract class Tag<T> {
-    @Getter
-    protected final T value;
-
-    protected Tag(T value) {
-        this.value = value;
-    }
-
-    /**
-     * NullField is a value which can be converted to {@link com.google.protobuf.NullValue}.
-     * Users should use the singleton instead of create a new instance everytime.
-     */
-    public static class NullField extends Tag<Object> implements SerializableTag<Banyandb.TagValue> {
-        private static final NullField INSTANCE = new NullField();
-
-        private NullField() {
-            super(null);
-        }
-
-        @Override
-        public Banyandb.TagValue toTag() {
-            return Banyandb.TagValue.newBuilder().setNull(NULL_VALUE).build();
-        }
-    }
-
-    /**
-     * The value of a String type field.
-     */
-    public static class StringField extends Tag<String> implements SerializableTag<Banyandb.TagValue> {
-        private StringField(String value) {
-            super(value);
-        }
-
-        @Override
-        public Banyandb.TagValue toTag() {
-            return Banyandb.TagValue.newBuilder().setStr(Banyandb.Str.newBuilder().setValue(value)).build();
-        }
-    }
-
-    /**
-     * The value of a String array type field.
-     */
-    public static class StringArrayField extends Tag<List<String>> implements SerializableTag<Banyandb.TagValue> {
-        private StringArrayField(List<String> value) {
-            super(value);
-        }
-
-        @Override
-        public Banyandb.TagValue toTag() {
-            return Banyandb.TagValue.newBuilder().setStrArray(Banyandb.StrArray.newBuilder().addAllValue(value)).build();
-        }
-    }
-
-    /**
-     * The value of an int64(Long) type field.
-     */
-    public static class LongField extends Tag<Long> implements SerializableTag<Banyandb.TagValue> {
-        private LongField(Long value) {
-            super(value);
-        }
-
-        @Override
-        public Banyandb.TagValue toTag() {
-            return Banyandb.TagValue.newBuilder().setInt(Banyandb.Int.newBuilder().setValue(value)).build();
-        }
-    }
-
-    /**
-     * The value of an int64(Long) array type field.
-     */
-    public static class LongArrayField extends Tag<List<Long>> implements SerializableTag<Banyandb.TagValue> {
-        private LongArrayField(List<Long> value) {
-            super(value);
-        }
-
-        @Override
-        public Banyandb.TagValue toTag() {
-            return Banyandb.TagValue.newBuilder().setIntArray(Banyandb.IntArray.newBuilder().addAllValue(value)).build();
-        }
-    }
-
-    /**
-     * The value of a byte array(ByteString) type field.
-     */
-    public static class BinaryField extends Tag<ByteString> implements SerializableTag<Banyandb.TagValue> {
-        public BinaryField(ByteString byteString) {
-            super(byteString);
-        }
-
-        @Override
-        public Banyandb.TagValue toTag() {
-            return Banyandb.TagValue.newBuilder().setBinaryData(value).build();
-        }
-    }
-
-    /**
-     * Construct a string field
-     *
-     * @param val payload
-     * @return Anonymous field with String payload
-     */
-    public static SerializableTag<Banyandb.TagValue> stringField(String val) {
-        return new StringField(val);
-    }
-
-    /**
-     * Construct a numeric field
-     *
-     * @param val payload
-     * @return Anonymous field with numeric payload
-     */
-    public static SerializableTag<Banyandb.TagValue> longField(long val) {
-        return new LongField(val);
-    }
-
-    /**
-     * Construct a string array field
-     *
-     * @param val payload
-     * @return Anonymous field with string array payload
-     */
-    public static SerializableTag<Banyandb.TagValue> stringArrayField(List<String> val) {
-        return new StringArrayField(val);
-    }
-
-    /**
-     * Construct a byte array field.
-     *
-     * @param bytes binary data
-     * @return Anonymous field with binary payload
-     */
-    public static SerializableTag<Banyandb.TagValue> binaryField(byte[] bytes) {
-        return new BinaryField(ByteString.copyFrom(bytes));
-    }
-
-    /**
-     * Construct a long array field
-     *
-     * @param val payload
-     * @return Anonymous field with numeric array payload
-     */
-    public static SerializableTag<Banyandb.TagValue> longArrayField(List<Long> val) {
-        return new LongArrayField(val);
-    }
-
-    public static SerializableTag<Banyandb.TagValue> nullField() {
-        return NullField.INSTANCE;
-    }
-}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/TagAndValue.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/TagAndValue.java
index db7a012..afab5b6 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/TagAndValue.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/TagAndValue.java
@@ -22,20 +22,18 @@ import java.util.List;
 
 import com.google.protobuf.ByteString;
 import lombok.EqualsAndHashCode;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
 
 /**
  * TagAndValue represents a value of column in the response
  */
 @EqualsAndHashCode(callSuper = true)
-public abstract class TagAndValue<T> extends Tag<T> {
-    protected final String tagFamilyName;
+public abstract class TagAndValue<T> extends Value<T> {
     protected final String tagName;
 
-    protected TagAndValue(String tagFamilyName, String fieldName, T value) {
+    protected TagAndValue(String tagName, T value) {
         super(value);
-        this.tagFamilyName = tagFamilyName;
-        this.tagName = fieldName;
+        this.tagName = tagName;
     }
 
     /**
@@ -45,13 +43,6 @@ public abstract class TagAndValue<T> extends Tag<T> {
         return this.tagName;
     }
 
-    /**
-     * @return tag family name
-     */
-    public String getTagFamilyName() {
-        return this.tagFamilyName;
-    }
-
     /**
      * @return true if value is null;
      */
@@ -59,58 +50,58 @@ public abstract class TagAndValue<T> extends Tag<T> {
         return this.value == null;
     }
 
-    static TagAndValue<?> build(String tagFamilyName, Banyandb.Tag tag) {
+    static TagAndValue<?> build(BanyandbModel.Tag tag) {
         switch (tag.getValue().getValueCase()) {
             case INT:
-                return new LongTagPair(tagFamilyName, tag.getKey(), tag.getValue().getInt().getValue());
+                return new LongTagPair(tag.getKey(), tag.getValue().getInt().getValue());
             case STR:
-                return new StringTagPair(tagFamilyName, tag.getKey(), tag.getValue().getStr().getValue());
+                return new StringTagPair(tag.getKey(), tag.getValue().getStr().getValue());
             case INT_ARRAY:
-                return new LongArrayTagPair(tagFamilyName, tag.getKey(), tag.getValue().getIntArray().getValueList());
+                return new LongArrayTagPair(tag.getKey(), tag.getValue().getIntArray().getValueList());
             case STR_ARRAY:
-                return new StringArrayTagPair(tagFamilyName, tag.getKey(), tag.getValue().getStrArray().getValueList());
+                return new StringArrayTagPair(tag.getKey(), tag.getValue().getStrArray().getValueList());
             case BINARY_DATA:
-                return new BinaryTagPair(tagFamilyName, tag.getKey(), tag.getValue().getBinaryData());
+                return new BinaryTagPair(tag.getKey(), tag.getValue().getBinaryData());
             case NULL:
-                return new NullTagPair(tagFamilyName, tag.getKey());
+                return new NullTagPair(tag.getKey());
             default:
                 throw new IllegalArgumentException("Unrecognized NullType");
         }
     }
 
     public static class StringTagPair extends TagAndValue<String> {
-        StringTagPair(final String tagFamilyName, final String tagName, final String value) {
-            super(tagFamilyName, tagName, value);
+        StringTagPair(final String tagName, final String value) {
+            super(tagName, value);
         }
     }
 
     public static class StringArrayTagPair extends TagAndValue<List<String>> {
-        StringArrayTagPair(final String tagFamilyName, final String tagName, final List<String> value) {
-            super(tagFamilyName, tagName, value);
+        StringArrayTagPair(final String tagName, final List<String> value) {
+            super(tagName, value);
         }
     }
 
     public static class LongTagPair extends TagAndValue<Long> {
-        LongTagPair(final String tagFamilyName, final String tagName, final Long value) {
-            super(tagFamilyName, tagName, value);
+        LongTagPair(final String tagName, final Long value) {
+            super(tagName, value);
         }
     }
 
     public static class LongArrayTagPair extends TagAndValue<List<Long>> {
-        LongArrayTagPair(final String tagFamilyName, final String tagName, final List<Long> value) {
-            super(tagFamilyName, tagName, value);
+        LongArrayTagPair(final String tagName, final List<Long> value) {
+            super(tagName, value);
         }
     }
 
     public static class BinaryTagPair extends TagAndValue<ByteString> {
-        public BinaryTagPair(String tagFamilyName, String fieldName, ByteString byteString) {
-            super(tagFamilyName, fieldName, byteString);
+        public BinaryTagPair(String fieldName, ByteString byteString) {
+            super(fieldName, byteString);
         }
     }
 
     public static class NullTagPair extends TagAndValue<Void> {
-        NullTagPair(final String tagFamilyName, final String tagName) {
-            super(tagFamilyName, tagName, null);
+        NullTagPair(final String tagName) {
+            super(tagName, null);
         }
 
         @Override
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/TimestampRange.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/TimestampRange.java
index e1f1f14..7ea4e3f 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/TimestampRange.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/TimestampRange.java
@@ -22,7 +22,7 @@ import com.google.protobuf.Timestamp;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
 
 @RequiredArgsConstructor
 @Getter(AccessLevel.PROTECTED)
@@ -40,14 +40,14 @@ public class TimestampRange {
     /**
      * @return TimeRange accordingly.
      */
-    Banyandb.TimeRange build() {
-        final Banyandb.TimeRange.Builder builder = Banyandb.TimeRange.newBuilder();
+    BanyandbModel.TimeRange build() {
+        final BanyandbModel.TimeRange.Builder builder = BanyandbModel.TimeRange.newBuilder();
         builder.setBegin(Timestamp.newBuilder()
-                                  .setSeconds(begin / 1000)
-                                  .setNanos((int) (begin % 1000 * 1_000_000)));
+                .setSeconds(begin / 1000)
+                .setNanos((int) (begin % 1000 * 1_000_000)));
         builder.setEnd(Timestamp.newBuilder()
-                                  .setSeconds(end / 1000)
-                                  .setNanos((int) (end % 1000 * 1_000_000)));
+                .setSeconds(end / 1000)
+                .setNanos((int) (end % 1000 * 1_000_000)));
         return builder.build();
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Value.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/Value.java
new file mode 100644
index 0000000..becdf59
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/Value.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import java.util.List;
+
+import com.google.common.base.Strings;
+import com.google.protobuf.ByteString;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.v1.client.metadata.Serializable;
+
+import static com.google.protobuf.NullValue.NULL_VALUE;
+
+/**
+ * Field represents a value in the write-op or response.
+ */
+@EqualsAndHashCode
+public abstract class Value<T> {
+    @Getter
+    protected final T value;
+
+    protected Value(T value) {
+        this.value = value;
+    }
+
+    /**
+     * NullTagValue is a value which can be converted to {@link com.google.protobuf.NullValue}.
+     * Users should use the singleton instead of create a new instance everytime.
+     */
+    public static class NullTagValue extends Value<Object> implements Serializable<BanyandbModel.TagValue> {
+        private static final NullTagValue INSTANCE = new NullTagValue();
+
+        private NullTagValue() {
+            super(null);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setNull(NULL_VALUE).build();
+        }
+    }
+
+    /**
+     * The value of a String type tag.
+     */
+    public static class StringTagValue extends Value<String> implements Serializable<BanyandbModel.TagValue> {
+        private StringTagValue(String value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setStr(BanyandbModel.Str.newBuilder().setValue(value)).build();
+        }
+    }
+
+    /**
+     * The value of a String array type tag.
+     */
+    public static class StringArrayTagValue extends Value<List<String>> implements Serializable<BanyandbModel.TagValue> {
+        private StringArrayTagValue(List<String> value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setStrArray(BanyandbModel.StrArray.newBuilder().addAllValue(value)).build();
+        }
+    }
+
+    public static class IDTagValue extends Value<String> implements Serializable<BanyandbModel.TagValue> {
+        private IDTagValue(String value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setId(BanyandbModel.ID.newBuilder().setValue(value)).build();
+        }
+    }
+
+    /**
+     * The value of an int64(Long) type tag.
+     */
+    public static class LongTagValue extends Value<Long> implements Serializable<BanyandbModel.TagValue> {
+        private LongTagValue(Long value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setInt(BanyandbModel.Int.newBuilder().setValue(value)).build();
+        }
+    }
+
+    /**
+     * The value of an int64(Long) array type tag.
+     */
+    public static class LongArrayTagValue extends Value<List<Long>> implements Serializable<BanyandbModel.TagValue> {
+        private LongArrayTagValue(List<Long> value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setIntArray(BanyandbModel.IntArray.newBuilder().addAllValue(value)).build();
+        }
+    }
+
+    /**
+     * The value of a byte array(ByteString) type tag.
+     */
+    public static class BinaryTagValue extends Value<ByteString> implements Serializable<BanyandbModel.TagValue> {
+        public BinaryTagValue(ByteString byteString) {
+            super(byteString);
+        }
+
+        @Override
+        public BanyandbModel.TagValue serialize() {
+            return BanyandbModel.TagValue.newBuilder().setBinaryData(value).build();
+        }
+    }
+
+    /**
+     * Construct a string tag
+     *
+     * @param val payload
+     * @return Anonymous tag with String payload
+     */
+    public static Serializable<BanyandbModel.TagValue> stringTagValue(String val) {
+        if (val == null) {
+            return nullTagValue();
+        }
+        return new StringTagValue(val);
+    }
+
+    public static Serializable<BanyandbModel.TagValue> idTagValue(String val) {
+        if (Strings.isNullOrEmpty(val)) {
+            throw new NullPointerException();
+        }
+        return new IDTagValue(val);
+    }
+
+    /**
+     * Construct a numeric tag
+     *
+     * @param val payload
+     * @return Anonymous tag with numeric payload
+     */
+    public static Serializable<BanyandbModel.TagValue> longTagValue(long val) {
+        return new LongTagValue(val);
+    }
+
+    /**
+     * Construct a string array tag
+     *
+     * @param val payload
+     * @return Anonymous tag with string array payload
+     */
+    public static Serializable<BanyandbModel.TagValue> stringArrayTagValue(List<String> val) {
+        return new StringArrayTagValue(val);
+    }
+
+    /**
+     * Construct a byte array tag.
+     *
+     * @param bytes binary data
+     * @return Anonymous tag with binary payload
+     */
+    public static Serializable<BanyandbModel.TagValue> binaryTagValue(byte[] bytes) {
+        return new BinaryTagValue(ByteString.copyFrom(bytes));
+    }
+
+    /**
+     * Construct a long array tag
+     *
+     * @param val payload
+     * @return Anonymous tag with numeric array payload
+     */
+    public static Serializable<BanyandbModel.TagValue> longArrayTag(List<Long> val) {
+        return new LongArrayTagValue(val);
+    }
+
+    public static Serializable<BanyandbModel.TagValue> nullTagValue() {
+        return NullTagValue.INSTANCE;
+    }
+
+    /**
+     * The value of a String type field.
+     */
+    public static class StringFieldValue extends Value<String> implements Serializable<BanyandbModel.FieldValue> {
+        private StringFieldValue(String value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.FieldValue serialize() {
+            return BanyandbModel.FieldValue.newBuilder().setStr(BanyandbModel.Str.newBuilder().setValue(value)).build();
+        }
+    }
+
+    public static Serializable<BanyandbModel.FieldValue> stringFieldValue(String val) {
+        if (Strings.isNullOrEmpty(val)) {
+            return nullFieldValue();
+        }
+        return new StringFieldValue(val);
+    }
+
+    /**
+     * NullFieldValue is a value which can be converted to {@link com.google.protobuf.NullValue}.
+     * Users should use the singleton instead of create a new instance everytime.
+     */
+    public static class NullFieldValue extends Value<Object> implements Serializable<BanyandbModel.FieldValue> {
+        private static final NullFieldValue INSTANCE = new NullFieldValue();
+
+        private NullFieldValue() {
+            super(null);
+        }
+
+        @Override
+        public BanyandbModel.FieldValue serialize() {
+            return BanyandbModel.FieldValue.newBuilder().setNull(NULL_VALUE).build();
+        }
+    }
+
+    public static Serializable<BanyandbModel.FieldValue> nullFieldValue() {
+        return NullFieldValue.INSTANCE;
+    }
+
+    /**
+     * The value of an int64(Long) type field.
+     */
+    public static class LongFieldValue extends Value<Long> implements Serializable<BanyandbModel.FieldValue> {
+        private LongFieldValue(Long value) {
+            super(value);
+        }
+
+        @Override
+        public BanyandbModel.FieldValue serialize() {
+            return BanyandbModel.FieldValue.newBuilder().setInt(BanyandbModel.Int.newBuilder().setValue(value)).build();
+        }
+    }
+
+    /**
+     * Construct a numeric tag
+     *
+     * @param val payload
+     * @return Anonymous tag with numeric payload
+     */
+    public static Serializable<BanyandbModel.FieldValue> longFieldValue(long val) {
+        return new LongFieldValue(val);
+    }
+
+    /**
+     * The value of a byte array(ByteString) type field.
+     */
+    public static class BinaryFieldValue extends Value<ByteString> implements Serializable<BanyandbModel.FieldValue> {
+        public BinaryFieldValue(ByteString byteString) {
+            super(byteString);
+        }
+
+        @Override
+        public BanyandbModel.FieldValue serialize() {
+            return BanyandbModel.FieldValue.newBuilder().setBinaryData(value).build();
+        }
+    }
+
+    /**
+     * Construct a byte array tag.
+     *
+     * @param bytes binary data
+     * @return Anonymous tag with binary payload
+     */
+    public static Serializable<BanyandbModel.FieldValue> binaryFieldValue(byte[] bytes) {
+        return new BinaryFieldValue(ByteString.copyFrom(bytes));
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/GRPCStreamServiceStatus.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/GRPCStreamServiceStatus.java
new file mode 100644
index 0000000..4783e89
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/GRPCStreamServiceStatus.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class GRPCStreamServiceStatus {
+    private volatile boolean status;
+
+    public GRPCStreamServiceStatus(boolean status) {
+        this.status = status;
+    }
+
+    public boolean isStatus() {
+        return status;
+    }
+
+    public void finished() {
+        this.status = true;
+    }
+
+    /**
+     * Wait until success status reported.
+     */
+    public void wait4Finish() {
+        long recheckCycle = 5;
+        long hasWaited = 0L;
+        long maxCycle = 30 * 1000L; // 30 seconds max.
+        while (!status) {
+            try2Sleep(recheckCycle);
+            hasWaited += recheckCycle;
+
+            if (recheckCycle >= maxCycle) {
+                log.warn("Collector traceSegment service doesn't response in {} seconds.", hasWaited / 1000);
+            } else {
+                recheckCycle = Math.min(recheckCycle * 2, maxCycle);
+            }
+        }
+    }
+
+    /**
+     * Try to sleep, and ignore the {@link InterruptedException}
+     *
+     * @param millis the length of time to sleep in milliseconds
+     */
+    private void try2Sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException ignored) {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/HandleExceptionsWith.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/HandleExceptionsWith.java
new file mode 100644
index 0000000..0802767
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/HandleExceptionsWith.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc;
+
+import com.google.common.collect.Sets;
+import io.grpc.Status;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBGrpcApiExceptionFactory;
+
+public class HandleExceptionsWith {
+    private HandleExceptionsWith() {
+    }
+
+    private static final BanyanDBGrpcApiExceptionFactory EXCEPTION_FACTORY = new BanyanDBGrpcApiExceptionFactory(
+            // Exceptions caused by network issues are retryable
+            Sets.newHashSet(Status.Code.UNAVAILABLE, Status.Code.DEADLINE_EXCEEDED)
+    );
+
+    /**
+     * call the underlying operation and get response from the future.
+     *
+     * @param respSupplier a supplier which returns response
+     * @param <RESP>       a generic type of user-defined gRPC response
+     * @return response in the type of  defined in the gRPC protocol
+     * @throws BanyanDBException if the execution of the future itself thrown an exception
+     */
+    public static <RESP, E extends BanyanDBException> RESP callAndTranslateApiException(SupplierWithIO<RESP, E> respSupplier) throws BanyanDBException {
+        try {
+            return respSupplier.get();
+        } catch (Exception exception) {
+            throw EXCEPTION_FACTORY.createException(exception);
+        }
+    }
+
+    @FunctionalInterface
+    public interface SupplierWithIO<T, E extends Throwable> {
+        T get() throws E;
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/MetadataClient.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/MetadataClient.java
new file mode 100644
index 0000000..c4bbbd9
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/MetadataClient.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc;
+
+import com.google.protobuf.GeneratedMessageV3;
+import io.grpc.stub.AbstractBlockingStub;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.NamedSchema;
+
+import java.util.List;
+
+/**
+ * abstract metadata client which defines CRUD operations for a specific kind of schema.
+ *
+ * @param <P> ProtoBuf: schema defined in ProtoBuf format
+ * @param <S> NamedSchema: Java implementation (POJO) which can be serialized to P
+ */
+public abstract class MetadataClient<STUB extends AbstractBlockingStub<STUB>, P extends GeneratedMessageV3, S extends NamedSchema<P>> {
+    protected final STUB stub;
+
+    protected MetadataClient(STUB stub) {
+        this.stub = stub;
+    }
+
+    /**
+     * Create a schema
+     *
+     * @param payload the schema to be created
+     * @throws BanyanDBException a wrapped exception to the underlying gRPC calls
+     */
+    public abstract void create(S payload) throws BanyanDBException;
+
+    /**
+     * Update the schema
+     *
+     * @param payload the schema which will be updated with the given name and group
+     * @throws BanyanDBException a wrapped exception to the underlying gRPC calls
+     */
+    public abstract void update(S payload) throws BanyanDBException;
+
+    /**
+     * Delete a schema
+     *
+     * @param group the group of the schema to be removed
+     * @param name  the name of the schema to be removed
+     * @return whether this schema is deleted
+     * @throws BanyanDBException a wrapped exception to the underlying gRPC calls
+     */
+    public abstract boolean delete(String group, String name) throws BanyanDBException;
+
+    /**
+     * Get a schema with name
+     *
+     * @param group the group of the schema to be found
+     * @param name  the name of the schema to be found
+     * @return the schema, null if not found
+     * @throws BanyanDBException a wrapped exception to the underlying gRPC calls
+     */
+    public abstract S get(String group, String name) throws BanyanDBException;
+
+    /**
+     * List all schemas with the same group name
+     *
+     * @return a list of schemas found
+     * @throws BanyanDBException a wrapped exception to the underlying gRPC calls
+     */
+    public abstract List<S> list(String group) throws BanyanDBException;
+
+    protected <REQ, RESP, E extends BanyanDBException> RESP execute(HandleExceptionsWith.SupplierWithIO<RESP, E> supplier) throws BanyanDBException {
+        return HandleExceptionsWith.callAndTranslateApiException(supplier);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/SerializableTag.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelFactory.java
similarity index 73%
rename from src/main/java/org/apache/skywalking/banyandb/v1/client/SerializableTag.java
rename to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelFactory.java
index 4230b1e..538bc64 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/SerializableTag.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelFactory.java
@@ -16,12 +16,12 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.channel;
 
-/**
- * An interface that represents an object which can be converted to the protobuf representation
- * of BanyanDB.Field. BanyanDB.Field is used for writing entities to the database.
- */
-public interface SerializableTag<T> {
-    T toTag();
+import io.grpc.ManagedChannel;
+
+import java.io.IOException;
+
+public interface ChannelFactory {
+    ManagedChannel create() throws IOException;
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManager.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManager.java
new file mode 100644
index 0000000..5a01994
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManager.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc.channel;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ConnectivityState;
+import io.grpc.ForwardingClientCall;
+import io.grpc.ForwardingClientCallListener;
+import io.grpc.ManagedChannel;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.NameResolverRegistry;
+import io.grpc.Status;
+import io.grpc.internal.DnsNameResolverProvider;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Slf4j
+public class ChannelManager extends ManagedChannel {
+    private final LazyReferenceChannel lazyChannel = new LazyReferenceChannel();
+
+    private static final Set<Status.Code> SC_NETWORK = ImmutableSet.of(
+            Status.Code.UNAVAILABLE, Status.Code.PERMISSION_DENIED,
+            Status.Code.UNAUTHENTICATED, Status.Code.RESOURCE_EXHAUSTED, Status.Code.UNKNOWN
+    );
+
+    private final ChannelManagerSettings settings;
+    private final ChannelFactory channelFactory;
+    private final ScheduledExecutorService executor;
+    @VisibleForTesting
+    final AtomicReference<Entry> entryRef = new AtomicReference<>();
+    private final String authority;
+
+    public static ChannelManager create(ChannelManagerSettings settings, ChannelFactory channelFactory)
+            throws IOException {
+        return new ChannelManager(settings, channelFactory, Executors.newSingleThreadScheduledExecutor());
+    }
+
+    public ChannelManager(ChannelManagerSettings settings, ChannelFactory channelFactory, ScheduledExecutorService executor) throws IOException {
+        this.settings = settings;
+        this.channelFactory = channelFactory;
+        this.executor = executor;
+
+        NameResolverRegistry.getDefaultRegistry().register(new DnsNameResolverProvider());
+
+        entryRef.set(new Entry(channelFactory.create()));
+        authority = entryRef.get().channel.authority();
+
+        this.executor.scheduleAtFixedRate(
+                this::refreshSafely,
+                settings.refreshInterval(),
+                settings.refreshInterval(),
+                TimeUnit.SECONDS
+        );
+    }
+
+    private void refreshSafely() {
+        try {
+            refresh();
+        } catch (Exception e) {
+            log.warn("Failed to refresh channels", e);
+        }
+    }
+
+    void refresh() throws IOException {
+        Entry entry = entryRef.get();
+        if (entry.needReconnect) {
+            if (entry.isConnected(entry.reconnectCount.incrementAndGet() > this.settings.forceReconnectionThreshold())) {
+                // Reconnect to the same server is automatically done by GRPC
+                // clear the flags
+                entry.reset();
+            } else {
+                Entry replacedEntry = entryRef.getAndSet(new Entry(this.channelFactory.create()));
+                replacedEntry.shutdown();
+            }
+        }
+    }
+
+    @Override
+    public ManagedChannel shutdown() {
+        entryRef.get().channel.shutdown();
+        if (executor != null) {
+            // shutdownNow will cancel scheduled tasks
+            executor.shutdownNow();
+        }
+        return this;
+    }
+
+    @Override
+    public boolean isShutdown() {
+        if (!this.entryRef.get().channel.isShutdown()) {
+            return false;
+        }
+        return executor == null || executor.isShutdown();
+    }
+
+    @Override
+    public boolean isTerminated() {
+        if (!this.entryRef.get().channel.isTerminated()) {
+            return false;
+        }
+        return executor == null || executor.isTerminated();
+    }
+
+    @Override
+    public ManagedChannel shutdownNow() {
+        entryRef.get().channel.shutdownNow();
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+        return this;
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+        long endTimeNanos = System.nanoTime() + unit.toNanos(timeout);
+        entryRef.get().channel.awaitTermination(endTimeNanos - System.nanoTime(), TimeUnit.NANOSECONDS);
+        if (executor != null) {
+            long awaitTimeNanos = endTimeNanos - System.nanoTime();
+            executor.awaitTermination(awaitTimeNanos, TimeUnit.NANOSECONDS);
+        }
+        return isTerminated();
+    }
+
+    @Override
+    public <REQ, RESP> ClientCall<REQ, RESP> newCall(MethodDescriptor<REQ, RESP> methodDescriptor, CallOptions callOptions) {
+        return lazyChannel.newCall(methodDescriptor, callOptions);
+    }
+
+    @Override
+    public String authority() {
+        return this.authority;
+    }
+
+    @RequiredArgsConstructor
+    static class Entry {
+        final ManagedChannel channel;
+
+        final AtomicInteger reconnectCount = new AtomicInteger(0);
+
+        volatile boolean needReconnect = false;
+
+        boolean isConnected(boolean requestConnection) {
+            return this.channel.getState(requestConnection) == ConnectivityState.READY;
+        }
+
+        void shutdown() {
+            this.channel.shutdown();
+        }
+
+        void reset() {
+            needReconnect = false;
+            reconnectCount.set(0);
+        }
+    }
+
+    private class LazyReferenceChannel extends Channel {
+        @Override
+        public <REQ, RESP> ClientCall<REQ, RESP> newCall(MethodDescriptor<REQ, RESP> methodDescriptor, CallOptions callOptions) {
+            Entry entry = entryRef.get();
+
+            return new NetworkExceptionAwareClientCall<>(entry.channel.newCall(methodDescriptor, callOptions), entry);
+        }
+
+        @Override
+        public String authority() {
+            return authority;
+        }
+    }
+
+    static class NetworkExceptionAwareClientCall<REQ, RESP> extends ForwardingClientCall.SimpleForwardingClientCall<REQ, RESP> {
+        final Entry entry;
+
+        public NetworkExceptionAwareClientCall(ClientCall<REQ, RESP> delegate, Entry entry) {
+            super(delegate);
+            this.entry = entry;
+        }
+
+        @Override
+        public void start(Listener<RESP> responseListener, Metadata headers) {
+            super.start(
+                    new ForwardingClientCallListener.SimpleForwardingClientCallListener<RESP>(responseListener) {
+                        @Override
+                        public void onClose(Status status, Metadata trailers) {
+                            if (isNetworkError(status)) {
+                                entry.needReconnect = true;
+                            }
+                            super.onClose(status, trailers);
+                        }
+                    },
+                    headers);
+        }
+    }
+
+    static boolean isNetworkError(Status status) {
+        return SC_NETWORK.contains(status.getCode());
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManagerSettings.java
similarity index 56%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManagerSettings.java
index 89b8038..4318277 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManagerSettings.java
@@ -16,28 +16,26 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.channel;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import com.google.auto.value.AutoValue;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+@AutoValue
+public abstract class ChannelManagerSettings {
+    abstract long refreshInterval();
+
+    abstract long forceReconnectionThreshold();
+
+    public static Builder newBuilder() {
+        return new AutoValue_ChannelManagerSettings.Builder();
     }
 
-}
\ No newline at end of file
+    @AutoValue.Builder
+    public abstract static class Builder {
+        public abstract Builder setRefreshInterval(long interval);
+
+        public abstract Builder setForceReconnectionThreshold(long threshold);
+
+        public abstract ChannelManagerSettings build();
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/DefaultChannelFactory.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/DefaultChannelFactory.java
new file mode 100644
index 0000000..40777a2
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/DefaultChannelFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc.channel;
+
+import com.google.common.base.Strings;
+import io.grpc.ManagedChannel;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NegotiationType;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.banyandb.v1.client.Options;
+import org.apache.skywalking.banyandb.v1.client.util.PrivateKeyUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+@Slf4j
+@RequiredArgsConstructor
+public class DefaultChannelFactory implements ChannelFactory {
+    private final String host;
+    private final int port;
+    private final Options options;
+
+    @Override
+    public ManagedChannel create() throws IOException {
+        NettyChannelBuilder managedChannelBuilder = NettyChannelBuilder.forAddress(this.host, this.port)
+                .maxInboundMessageSize(options.getMaxInboundMessageSize())
+                .usePlaintext();
+
+        File caFile = new File(options.getSslTrustCAPath());
+        boolean isCAFileExist = caFile.exists() && caFile.isFile();
+        if (options.isForceTLS() || isCAFileExist) {
+            SslContextBuilder builder = GrpcSslContexts.forClient();
+
+            if (isCAFileExist) {
+                String certPath = options.getSslCertChainPath();
+                String keyPath = options.getSslKeyPath();
+                if (!Strings.isNullOrEmpty(certPath) && Strings.isNullOrEmpty(keyPath)) {
+                    File keyFile = new File(keyPath);
+                    File certFile = new File(certPath);
+
+                    if (certFile.isFile() && keyFile.isFile()) {
+                        try (InputStream cert = new FileInputStream(certFile);
+                             InputStream key = PrivateKeyUtil.loadDecryptionKey(keyFile.getAbsolutePath())) {
+                            builder.keyManager(cert, key);
+                        }
+                    } else if (!certFile.isFile() || !keyFile.isFile()) {
+                        log.warn("Failed to enable mTLS caused by cert or key cannot be found.");
+                    }
+                }
+
+                builder.trustManager(caFile);
+            }
+            managedChannelBuilder.negotiationType(NegotiationType.TLS).sslContext(builder.build());
+        }
+        return managedChannelBuilder.build();
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/AbortedException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/AbortedException.java
index 9732233..1b38509 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/AbortedException.java
@@ -16,17 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client.metadata;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import io.grpc.Status;
 
-@RequiredArgsConstructor
-public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+public class AbortedException extends BanyanDBException {
+    public AbortedException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
+    }
 
-    @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+    public AbortedException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/AlreadyExistsException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/AlreadyExistsException.java
index 89b8038..a047f99 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/AlreadyExistsException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class AlreadyExistsException extends BanyanDBException {
+    public AlreadyExistsException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public AlreadyExistsException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBApiExceptionFactory.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBApiExceptionFactory.java
new file mode 100644
index 0000000..d1f6e12
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBApiExceptionFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
+
+import io.grpc.Status;
+
+class BanyanDBApiExceptionFactory {
+    private BanyanDBApiExceptionFactory() {
+    }
+
+    public static BanyanDBException createException(Throwable cause, Status.Code statusCode, boolean retryable) {
+        switch (statusCode) {
+            case CANCELLED:
+                return new CancelledException(cause, statusCode, retryable);
+            case NOT_FOUND:
+                return new NotFoundException(cause, statusCode, retryable);
+            case INVALID_ARGUMENT:
+                return new InvalidArgumentException(cause, statusCode, retryable);
+            case DEADLINE_EXCEEDED:
+                return new DeadlineExceededException(cause, statusCode, retryable);
+            case ALREADY_EXISTS:
+                return new AlreadyExistsException(cause, statusCode, retryable);
+            case PERMISSION_DENIED:
+                return new PermissionDeniedException(cause, statusCode, retryable);
+            case RESOURCE_EXHAUSTED:
+                return new ResourceExhaustedException(cause, statusCode, retryable);
+            case FAILED_PRECONDITION:
+                return new FailedPreconditionException(cause, statusCode, retryable);
+            case ABORTED:
+                return new AbortedException(cause, statusCode, retryable);
+            case OUT_OF_RANGE:
+                return new OutOfRangeException(cause, statusCode, retryable);
+            case UNIMPLEMENTED:
+                return new UnimplementedException(cause, statusCode, retryable);
+            case INTERNAL:
+                return new InternalException(cause, statusCode, retryable);
+            case UNAVAILABLE:
+                return new UnavailableException(cause, statusCode, retryable);
+            case DATA_LOSS:
+                return new DataLossException(cause, statusCode, retryable);
+            case UNAUTHENTICATED:
+                return new UnauthenticatedException(cause, statusCode, retryable);
+            case UNKNOWN: // Fall through.
+            default:
+                return new UnknownException(cause, statusCode, retryable);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBException.java
similarity index 52%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBException.java
index 22c5faa..82f8d47 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/StreamQueryResponse.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBException.java
@@ -16,31 +16,29 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
-
-import java.util.ArrayList;
-import java.util.List;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
+import com.google.common.base.Preconditions;
+import io.grpc.Status;
 import lombok.Getter;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
 
 /**
- * StreamQueryResponse represents the stream query result.
+ * BanyanDBException represents an exception thrown during a gRPC call.
  */
-public class StreamQueryResponse {
-    @Getter
-    private final List<RowEntity> elements;
+@Getter
+public class BanyanDBException extends Exception {
+    private final Status.Code status;
+    private final boolean retryable;
 
-    StreamQueryResponse(BanyandbStream.QueryResponse response) {
-        final List<BanyandbStream.Element> elementsList = response.getElementsList();
-        elements = new ArrayList<>(elementsList.size());
-        elementsList.forEach(element -> elements.add(new RowEntity(element)));
+    public BanyanDBException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause);
+        this.status = Preconditions.checkNotNull(status);
+        this.retryable = retryable;
     }
 
-    /**
-     * @return size of the response set.
-     */
-    public int size() {
-        return elements.size();
+    public BanyanDBException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause);
+        this.status = Preconditions.checkNotNull(status);
+        this.retryable = retryable;
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBGrpcApiExceptionFactory.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBGrpcApiExceptionFactory.java
new file mode 100644
index 0000000..c1008ce
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/BanyanDBGrpcApiExceptionFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
+
+import com.google.common.collect.ImmutableSet;
+import io.grpc.Status;
+import io.grpc.StatusException;
+import io.grpc.StatusRuntimeException;
+
+import java.util.Set;
+
+public class BanyanDBGrpcApiExceptionFactory {
+    private final ImmutableSet<Status.Code> retryableCodes;
+
+    public BanyanDBGrpcApiExceptionFactory(Set<Status.Code> retryCodes) {
+        this.retryableCodes = ImmutableSet.copyOf(retryCodes);
+    }
+
+    public BanyanDBException createException(Throwable throwable) {
+        if (throwable instanceof StatusException) {
+            StatusException e = (StatusException) throwable;
+            return create(throwable, e.getStatus().getCode());
+        } else if (throwable instanceof StatusRuntimeException) {
+            StatusRuntimeException e = (StatusRuntimeException) throwable;
+            return create(throwable, e.getStatus().getCode());
+        } else if (throwable instanceof BanyanDBException) {
+            return (BanyanDBException) throwable;
+        } else {
+            // Do not retry on unknown throwable, even when UNKNOWN is in retryableCodes
+            return BanyanDBApiExceptionFactory.createException(
+                    throwable, Status.Code.UNKNOWN, false);
+        }
+    }
+
+    private BanyanDBException create(Throwable throwable, Status.Code statusCode) {
+        boolean retryable = retryableCodes.contains(statusCode);
+        return BanyanDBApiExceptionFactory.createException(throwable, statusCode, retryable);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/CancelledException.java
similarity index 63%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/CancelledException.java
index 89b8038..b1683f1 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/CancelledException.java
@@ -16,28 +16,17 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class CancelledException extends BanyanDBException {
+    public CancelledException(Throwable cause, Status.Code statusCode, boolean retryable) {
+        super(cause, statusCode, retryable);
     }
 
-}
\ No newline at end of file
+    public CancelledException(
+            String message, Throwable cause, Status.Code statusCode, boolean retryable) {
+        super(message, cause, statusCode, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/DataLossException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/DataLossException.java
index 9732233..7751097 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/DataLossException.java
@@ -16,17 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client.metadata;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import io.grpc.Status;
 
-@RequiredArgsConstructor
-public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+public class DataLossException extends BanyanDBException {
+    public DataLossException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
+    }
 
-    @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+    public DataLossException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/DeadlineExceededException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/DeadlineExceededException.java
index 89b8038..57168a5 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/DeadlineExceededException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class DeadlineExceededException extends BanyanDBException {
+    public DeadlineExceededException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public DeadlineExceededException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/FailedPreconditionException.java
similarity index 63%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/FailedPreconditionException.java
index 89b8038..40e5ba7 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/FailedPreconditionException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class FailedPreconditionException extends BanyanDBException {
+    public FailedPreconditionException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public FailedPreconditionException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InternalException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InternalException.java
index 9732233..6954ce8 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InternalException.java
@@ -16,17 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client.metadata;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import io.grpc.Status;
 
-@RequiredArgsConstructor
-public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+public class InternalException extends BanyanDBException {
+    public InternalException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
+    }
 
-    @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+    public InternalException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InvalidArgumentException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InvalidArgumentException.java
index 89b8038..df8412b 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InvalidArgumentException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class InvalidArgumentException extends BanyanDBException {
+    public InvalidArgumentException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public InvalidArgumentException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InvalidReferenceException.java
similarity index 53%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InvalidReferenceException.java
index 89b8038..dcbbc78 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/InvalidReferenceException.java
@@ -16,28 +16,23 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
+public class InvalidReferenceException extends BanyanDBException {
+    private final String refName;
+
+    private InvalidReferenceException(String refName, String message) {
+        super(message, null, Status.Code.INVALID_ARGUMENT, false);
+        this.refName = refName;
+    }
 
-    Options() {
+    public static InvalidReferenceException fromInvalidTag(String tagName) {
+        return new InvalidReferenceException(tagName, "invalid ref to tag " + tagName);
     }
 
-}
\ No newline at end of file
+    public static InvalidReferenceException fromInvalidField(String fieldName) {
+        return new InvalidReferenceException(fieldName, "invalid ref to field " + fieldName);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/NotFoundException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/NotFoundException.java
index 9732233..148bad6 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/NotFoundException.java
@@ -16,17 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client.metadata;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import io.grpc.Status;
 
-@RequiredArgsConstructor
-public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+public class NotFoundException extends BanyanDBException {
+    public NotFoundException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
+    }
 
-    @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+    public NotFoundException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/OutOfRangeException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/OutOfRangeException.java
index 89b8038..d2c7002 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/OutOfRangeException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class OutOfRangeException extends BanyanDBException {
+    public OutOfRangeException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public OutOfRangeException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/PermissionDeniedException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/PermissionDeniedException.java
index 89b8038..e11a2bb 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/PermissionDeniedException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class PermissionDeniedException extends BanyanDBException {
+    public PermissionDeniedException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public PermissionDeniedException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/ResourceExhaustedException.java
similarity index 63%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/ResourceExhaustedException.java
index 89b8038..89d5c3b 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/ResourceExhaustedException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class ResourceExhaustedException extends BanyanDBException {
+    public ResourceExhaustedException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public ResourceExhaustedException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnauthenticatedException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnauthenticatedException.java
index 89b8038..4957d10 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnauthenticatedException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class UnauthenticatedException extends BanyanDBException {
+    public UnauthenticatedException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public UnauthenticatedException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnavailableException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnavailableException.java
index 89b8038..bb127de 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnavailableException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class UnavailableException extends BanyanDBException {
+    public UnavailableException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public UnavailableException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnimplementedException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnimplementedException.java
index 89b8038..888924e 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/Options.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnimplementedException.java
@@ -16,28 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import io.grpc.Status;
 
-/**
- * Client connection options.
- */
-@Setter
-@Getter(AccessLevel.PACKAGE)
-public class Options {
-    /**
-     * Max inbound message size
-     */
-    private int maxInboundMessageSize = 1024 * 1024 * 50;
-    /**
-     * Threshold of gRPC blocking query, unit is second
-     */
-    private int deadline = 30;
-
-    Options() {
+public class UnimplementedException extends BanyanDBException {
+    public UnimplementedException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
     }
 
-}
\ No newline at end of file
+    public UnimplementedException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnknownException.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnknownException.java
index 9732233..bb0475e 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/grpc/exception/UnknownException.java
@@ -16,17 +16,16 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client.metadata;
+package org.apache.skywalking.banyandb.v1.client.grpc.exception;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import io.grpc.Status;
 
-@RequiredArgsConstructor
-public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+public class UnknownException extends BanyanDBException {
+    public UnknownException(Throwable cause, Status.Code status, boolean retryable) {
+        super(cause, status, retryable);
+    }
 
-    @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+    public UnknownException(String message, Throwable cause, Status.Code status, boolean retryable) {
+        super(message, cause, status, retryable);
+    }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
index 9732233..b143d6d 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
@@ -21,12 +21,14 @@ package org.apache.skywalking.banyandb.v1.client.metadata;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
 
 @RequiredArgsConstructor
 public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+    UNSPECIFIED(BanyandbCommon.Catalog.CATALOG_UNSPECIFIED),
+    STREAM(BanyandbCommon.Catalog.CATALOG_STREAM),
+    MEASURE(BanyandbCommon.Catalog.CATALOG_MEASURE);
 
     @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+    private final BanyandbCommon.Catalog catalog;
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Duration.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Duration.java
index 77b3d38..060cd76 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Duration.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Duration.java
@@ -18,92 +18,116 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import lombok.AccessLevel;
+import com.google.common.base.Strings;
 import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 @EqualsAndHashCode
-public class Duration implements Serializable<BanyandbMetadata.Duration> {
-    private final int val;
-    private final Unit unit;
+public class Duration {
+    private static final Pattern DURATION_PATTERN =
+            Pattern.compile("(((?<year>[0-9]+)y)?((?<week>[0-9]+)w)?((?<day>[0-9]+)d)?((?<hour>[0-9]+)h)?((?<minute>[0-9]+)m)?|0)");
+    private static final long MINUTES_PER_HOUR = 60;
+    private static final long MINUTES_PER_DAY = MINUTES_PER_HOUR * 24;
+    private static final long MINUTES_PER_WEEK = MINUTES_PER_DAY * 7;
+    private static final long MINUTES_PER_YEAR = MINUTES_PER_DAY * 365;
+
+    @EqualsAndHashCode.Exclude
+    private volatile String text;
+    private final long minutes;
 
-    private Duration(int val, Unit unit) {
-        this.val = val;
-        this.unit = unit;
+    private Duration(long minutes) {
+        this.minutes = minutes;
     }
 
-    /**
-     * Create duration with hours
-     *
-     * @param hours the number of hours
-     * @return Duration in the unit of hour
-     */
-    public static Duration ofHours(int hours) {
-        return new Duration(hours, Unit.HOUR);
+    public String format() {
+        if (!Strings.isNullOrEmpty(text)) {
+            return text;
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        long minutes = this.minutes;
+        if (minutes >= MINUTES_PER_YEAR) {
+            long years = minutes / MINUTES_PER_YEAR;
+            builder.append(years).append("y");
+            minutes = minutes % MINUTES_PER_YEAR;
+        }
+        if (minutes >= MINUTES_PER_WEEK) {
+            long weeks = minutes / MINUTES_PER_WEEK;
+            builder.append(weeks).append("w");
+            minutes = minutes % MINUTES_PER_WEEK;
+        }
+        if (minutes >= MINUTES_PER_DAY) {
+            long weeks = minutes / MINUTES_PER_DAY;
+            builder.append(weeks).append("d");
+            minutes = minutes % MINUTES_PER_DAY;
+        }
+        if (minutes >= MINUTES_PER_HOUR) {
+            long weeks = minutes / MINUTES_PER_HOUR;
+            builder.append(weeks).append("h");
+            minutes = minutes % MINUTES_PER_HOUR;
+        }
+        if (minutes > 0) {
+            builder.append(minutes).append("m");
+        }
+        this.text = builder.toString();
+        return this.text;
     }
 
-    /**
-     * Create duration with days
-     *
-     * @param days the number of days
-     * @return Duration in the unit of day
-     */
-    public static Duration ofDays(int days) {
-        return new Duration(days, Unit.DAY);
+    public Duration add(Duration duration) {
+        return new Duration(this.minutes + duration.minutes);
     }
 
-    /**
-     * Create duration with weeks
-     *
-     * @param weeks the number of weeks
-     * @return Duration in the unit of week
-     */
-    public static Duration ofWeeks(int weeks) {
-        return new Duration(weeks, Unit.WEEK);
+    public static Duration parse(String text) {
+        if (Strings.isNullOrEmpty(text)) {
+            return new Duration(0);
+        }
+        Matcher matcher = DURATION_PATTERN.matcher(text);
+        if (!matcher.find()) {
+            return new Duration(0);
+        }
+        long total = 0;
+        final String years = matcher.group("year");
+        if (!Strings.isNullOrEmpty(years)) {
+            total += Long.parseLong(years) * MINUTES_PER_YEAR;
+        }
+        final String weeks = matcher.group("week");
+        if (!Strings.isNullOrEmpty(weeks)) {
+            total += Long.parseLong(weeks) * MINUTES_PER_WEEK;
+        }
+        final String days = matcher.group("day");
+        if (!Strings.isNullOrEmpty(days)) {
+            total += Long.parseLong(days) * MINUTES_PER_DAY;
+        }
+        final String hours = matcher.group("hour");
+        if (!Strings.isNullOrEmpty(hours)) {
+            total += Long.parseLong(hours) * MINUTES_PER_HOUR;
+        }
+        final String minutes = matcher.group("minute");
+        if (!Strings.isNullOrEmpty(minutes)) {
+            total += Long.parseLong(minutes);
+        }
+        return new Duration(total);
     }
 
-    /**
-     * Create duration with months
-     *
-     * @param months the number of months
-     * @return Duration in the unit of month
-     */
-    public static Duration ofMonths(int months) {
-        return new Duration(months, Unit.MONTH);
+    public static Duration ofMinutes(long minutes) {
+        return new Duration(minutes);
     }
 
-    @Override
-    public BanyandbMetadata.Duration serialize() {
-        return BanyandbMetadata.Duration.newBuilder()
-                .setVal(this.val)
-                .setUnit(this.unit.getDurationUnit())
-                .build();
+    public static Duration ofHours(long hours) {
+        return new Duration(hours * MINUTES_PER_HOUR);
     }
 
-    @RequiredArgsConstructor
-    public enum Unit {
-        HOUR(BanyandbMetadata.Duration.DurationUnit.DURATION_UNIT_HOUR),
-        DAY(BanyandbMetadata.Duration.DurationUnit.DURATION_UNIT_DAY),
-        WEEK(BanyandbMetadata.Duration.DurationUnit.DURATION_UNIT_WEEK),
-        MONTH(BanyandbMetadata.Duration.DurationUnit.DURATION_UNIT_MONTH);
+    public static Duration ofDays(long days) {
+        return ofHours(days * MINUTES_PER_DAY);
+    }
 
-        @Getter(AccessLevel.PRIVATE)
-        private final BanyandbMetadata.Duration.DurationUnit durationUnit;
+    public static Duration ofWeeks(long weeks) {
+        return ofHours(weeks * MINUTES_PER_WEEK);
     }
 
-    static Duration fromProtobuf(BanyandbMetadata.Duration duration) {
-        switch (duration.getUnit()) {
-            case DURATION_UNIT_DAY:
-                return ofDays(duration.getVal());
-            case DURATION_UNIT_HOUR:
-                return ofHours(duration.getVal());
-            case DURATION_UNIT_MONTH:
-                return ofMonths(duration.getVal());
-            case DURATION_UNIT_WEEK:
-                return ofWeeks(duration.getVal());
-        }
-        throw new IllegalArgumentException("unrecognized DurationUnit");
+    public static Duration ofYears(long years) {
+        return ofHours(years * MINUTES_PER_YEAR);
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Group.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Group.java
new file mode 100644
index 0000000..bb349dc
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Group.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.metadata;
+
+import com.google.auto.value.AutoValue;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.v1.client.util.TimeUtils;
+
+import javax.annotation.Nullable;
+import java.time.ZonedDateTime;
+
+@AutoValue
+public abstract class Group extends NamedSchema<BanyandbCommon.Group> {
+    /**
+     * catalog denotes which type of data the group contains
+     */
+    abstract Catalog catalog();
+
+    /**
+     * shard_num is the number of shards in this group
+     */
+    abstract int shardNum();
+
+    @Nullable
+    abstract Integer blockNum();
+
+    abstract Duration ttl();
+
+    public static Group create(String name, Catalog catalog, int shardNum, int blockNum, Duration ttl) {
+        return new AutoValue_Group(null, name, null, catalog, shardNum, blockNum, ttl);
+    }
+
+    public static Group create(String name, Catalog catalog, int shardNum, int blockNum, Duration ttl, ZonedDateTime updatedAt) {
+        return new AutoValue_Group(null, name, updatedAt, catalog, shardNum, blockNum, ttl);
+    }
+
+    @Override
+    public BanyandbCommon.Group serialize() {
+        return BanyandbCommon.Group.newBuilder()
+                // use name as the group
+                .setMetadata(this.buildMetadata().toBuilder())
+                .setCatalog(catalog().getCatalog())
+                .setResourceOpts(BanyandbCommon.ResourceOpts.newBuilder()
+                        .setShardNum(shardNum())
+                        .setBlockNum(blockNum())
+                        .setTtl(ttl().format())
+                        .build())
+                .build();
+    }
+
+    public static Group fromProtobuf(BanyandbCommon.Group group) {
+        Catalog catalog = Catalog.UNSPECIFIED;
+        switch (group.getCatalog()) {
+            case CATALOG_STREAM:
+                catalog = Catalog.STREAM;
+                break;
+            case CATALOG_MEASURE:
+                catalog = Catalog.MEASURE;
+                break;
+        }
+
+        return new AutoValue_Group(null,
+                group.getMetadata().getName(),
+                TimeUtils.parseTimestamp(group.getUpdatedAt()),
+                catalog,
+                group.getResourceOpts().getShardNum(),
+                group.getResourceOpts().getBlockNum(),
+                Duration.parse(group.getResourceOpts().getTtl()));
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/GroupMetadataRegistry.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/GroupMetadataRegistry.java
new file mode 100644
index 0000000..d5828a8
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/GroupMetadataRegistry.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.metadata;
+
+import io.grpc.Channel;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.GroupRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.MetadataClient;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class GroupMetadataRegistry extends MetadataClient<GroupRegistryServiceGrpc.GroupRegistryServiceBlockingStub,
+        BanyandbCommon.Group, Group> {
+
+    public GroupMetadataRegistry(Channel channel) {
+        super(GroupRegistryServiceGrpc.newBlockingStub(channel));
+    }
+
+    @Override
+    public void create(Group payload) throws BanyanDBException {
+        execute(() -> stub.create(BanyandbDatabase.GroupRegistryServiceCreateRequest.newBuilder()
+                .setGroup(payload.serialize())
+                .build()));
+    }
+
+    @Override
+    public void update(Group payload) throws BanyanDBException {
+        execute(() -> stub.update(BanyandbDatabase.GroupRegistryServiceUpdateRequest.newBuilder()
+                .setGroup(payload.serialize())
+                .build()));
+    }
+
+    @Override
+    public boolean delete(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.GroupRegistryServiceDeleteResponse resp = execute(() ->
+                stub.delete(BanyandbDatabase.GroupRegistryServiceDeleteRequest.newBuilder()
+                        .setGroup(name)
+                        .build()));
+        return resp != null && resp.getDeleted();
+    }
+
+    @Override
+    public Group get(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.GroupRegistryServiceGetResponse resp = execute(() ->
+                stub.get(BanyandbDatabase.GroupRegistryServiceGetRequest.newBuilder()
+                        .setGroup(name)
+                        .build()));
+
+        return Group.fromProtobuf(resp.getGroup());
+    }
+
+    @Override
+    public List<Group> list(String group) throws BanyanDBException {
+        BanyandbDatabase.GroupRegistryServiceListResponse resp = execute(() ->
+                stub.list(BanyandbDatabase.GroupRegistryServiceListRequest.newBuilder()
+                        .build()));
+
+        return resp.getGroupList().stream().map(Group::fromProtobuf).collect(Collectors.toList());
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRule.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRule.java
index 4771bef..c8bbf3c 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRule.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRule.java
@@ -18,89 +18,107 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import lombok.RequiredArgsConstructor;
-import lombok.Setter;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
 import org.apache.skywalking.banyandb.v1.client.util.TimeUtils;
 
 import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.List;
 
-@Setter
-@Getter
-@EqualsAndHashCode(callSuper = true)
-public class IndexRule extends NamedSchema<BanyandbMetadata.IndexRule> {
+@AutoValue
+public abstract class IndexRule extends NamedSchema<BanyandbDatabase.IndexRule> {
     /**
      * tags are the combination that refers to an indexed object
      * If the elements in tags are more than 1, the object will generate a multi-tag index
      * Caveat: All tags in a multi-tag MUST have an identical IndexType
      */
-    private List<String> tags;
+    abstract ImmutableList<String> tags();
 
     /**
      * indexType determine the index structure under the hood
      */
-    private IndexType indexType;
+    abstract IndexType indexType();
 
     /**
      * indexLocation indicates where to store index.
      */
-    private IndexLocation indexLocation;
+    abstract IndexLocation indexLocation();
 
-    public IndexRule(String name, IndexType indexType, IndexLocation indexLocation) {
-        this(name, indexType, indexLocation, null);
+    abstract Builder toBuilder();
+
+    public final IndexRule withGroup(String group) {
+        return toBuilder().setGroup(group).build();
     }
 
-    private IndexRule(String name, IndexType indexType, IndexLocation indexLocation, ZonedDateTime updatedAt) {
-        super(name, updatedAt);
-        this.tags = new ArrayList<>();
-        this.indexType = indexType;
-        this.indexLocation = indexLocation;
+    public static IndexRule create(String name, IndexType indexType, IndexLocation indexLocation) {
+        return new AutoValue_IndexRule.Builder().setName(name)
+                .setTags(ImmutableList.of(name))
+                .setIndexType(indexType)
+                .setIndexLocation(indexLocation)
+                .build();
     }
 
-    /**
-     * Add tag to the index rule
-     *
-     * @param tag the name of the tag to be appended
-     */
-    public IndexRule addTag(String tag) {
-        this.tags.add(tag);
-        return this;
+    @VisibleForTesting
+    static IndexRule create(String group, String name, IndexType indexType, IndexLocation indexLocation) {
+        return new AutoValue_IndexRule.Builder().setGroup(group).setName(name)
+                .setTags(ImmutableList.of(name))
+                .setIndexType(indexType)
+                .setIndexLocation(indexLocation)
+                .build();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+        abstract Builder setGroup(String group);
+
+        abstract Builder setName(String name);
+
+        abstract Builder setTags(ImmutableList<String> tags);
+
+        abstract Builder setIndexType(IndexType indexType);
+
+        abstract Builder setIndexLocation(IndexLocation indexLocation);
+
+        abstract Builder setUpdatedAt(ZonedDateTime updatedAt);
+
+        abstract IndexRule build();
     }
 
     @Override
-    public BanyandbMetadata.IndexRule serialize(String group) {
-        BanyandbMetadata.IndexRule.Builder b = BanyandbMetadata.IndexRule.newBuilder()
-                .setMetadata(buildMetadata(group))
-                .addAllTags(this.tags)
-                .setLocation(this.indexLocation.location)
-                .setType(this.indexType.type);
-
-        if (this.updatedAt != null) {
-            b.setUpdatedAt(TimeUtils.buildTimestamp(this.updatedAt));
+    public BanyandbDatabase.IndexRule serialize() {
+        final BanyandbDatabase.IndexRule.Builder b = BanyandbDatabase.IndexRule.newBuilder()
+                .setMetadata(buildMetadata())
+                .addAllTags(tags())
+                .setLocation(indexLocation().location)
+                .setType(indexType().type);
+
+        if (updatedAt() != null) {
+            b.setUpdatedAt(TimeUtils.buildTimestamp(updatedAt()));
         }
         return b.build();
     }
 
-    static IndexRule fromProtobuf(BanyandbMetadata.IndexRule pb) {
+    public static IndexRule fromProtobuf(BanyandbDatabase.IndexRule pb) {
         IndexType indexType = IndexType.fromProtobuf(pb.getType());
         IndexLocation indexLocation = IndexLocation.fromProtobuf(pb.getLocation());
-        IndexRule indexRule = new IndexRule(pb.getMetadata().getName(), indexType, indexLocation,
-                TimeUtils.parseTimestamp(pb.getUpdatedAt()));
-        indexRule.setTags(new ArrayList<>(pb.getTagsList()));
-        return indexRule;
+        return new AutoValue_IndexRule.Builder()
+                .setGroup(pb.getMetadata().getGroup())
+                .setName(pb.getMetadata().getName())
+                .setUpdatedAt(TimeUtils.parseTimestamp(pb.getUpdatedAt()))
+                .setIndexLocation(indexLocation)
+                .setIndexType(indexType)
+                .setTags(ImmutableList.copyOf(pb.getTagsList())).build();
     }
 
     @RequiredArgsConstructor
     public enum IndexType {
-        TREE(BanyandbMetadata.IndexRule.Type.TYPE_TREE), INVERTED(BanyandbMetadata.IndexRule.Type.TYPE_INVERTED);
+        TREE(BanyandbDatabase.IndexRule.Type.TYPE_TREE), INVERTED(BanyandbDatabase.IndexRule.Type.TYPE_INVERTED);
 
-        private final BanyandbMetadata.IndexRule.Type type;
+        private final BanyandbDatabase.IndexRule.Type type;
 
-        private static IndexType fromProtobuf(BanyandbMetadata.IndexRule.Type type) {
+        private static IndexType fromProtobuf(BanyandbDatabase.IndexRule.Type type) {
             switch (type) {
                 case TYPE_TREE:
                     return TREE;
@@ -114,11 +132,11 @@ public class IndexRule extends NamedSchema<BanyandbMetadata.IndexRule> {
 
     @RequiredArgsConstructor
     public enum IndexLocation {
-        SERIES(BanyandbMetadata.IndexRule.Location.LOCATION_SERIES), GLOBAL(BanyandbMetadata.IndexRule.Location.LOCATION_GLOBAL);
+        SERIES(BanyandbDatabase.IndexRule.Location.LOCATION_SERIES), GLOBAL(BanyandbDatabase.IndexRule.Location.LOCATION_GLOBAL);
 
-        private final BanyandbMetadata.IndexRule.Location location;
+        private final BanyandbDatabase.IndexRule.Location location;
 
-        private static IndexLocation fromProtobuf(BanyandbMetadata.IndexRule.Location loc) {
+        private static IndexLocation fromProtobuf(BanyandbDatabase.IndexRule.Location loc) {
             switch (loc) {
                 case LOCATION_GLOBAL:
                     return GLOBAL;
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBinding.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBinding.java
index f0430fc..ddd0837 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBinding.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBinding.java
@@ -18,92 +18,89 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import lombok.Setter;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
 import org.apache.skywalking.banyandb.v1.client.util.TimeUtils;
 
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
-import java.util.ArrayList;
 import java.util.List;
 
-@Setter
-@Getter
-@EqualsAndHashCode(callSuper = true)
-public class IndexRuleBinding extends NamedSchema<BanyandbMetadata.IndexRuleBinding> {
+@AutoValue
+public abstract class IndexRuleBinding extends NamedSchema<BanyandbDatabase.IndexRuleBinding> {
     private static final ZonedDateTime DEFAULT_EXPIRE_AT = ZonedDateTime.of(2099, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+
     /**
      * rule names refer to the IndexRule
      */
-    private List<String> rules;
+    public abstract ImmutableList<String> rules();
 
     /**
      * subject indicates the subject of binding action
      */
-    private final Subject subject;
+    abstract Subject subject();
 
     /**
      * begin_at is the timestamp, after which the binding will be active
      */
-    private ZonedDateTime beginAt;
+    abstract ZonedDateTime beginAt();
 
     /**
      * expire_at is the timestamp, after which the binding will be inactive
      * expire_at must be larger than begin_at
      */
-    private ZonedDateTime expireAt;
+    abstract ZonedDateTime expireAt();
 
-    public IndexRuleBinding(String name, Subject subject) {
-        this(name, subject, null);
+    public static IndexRuleBinding create(String group, String name, Subject subject, List<String> rules) {
+        return new AutoValue_IndexRuleBinding(group, name, null,
+                ImmutableList.copyOf(rules), subject, ZonedDateTime.now(), DEFAULT_EXPIRE_AT);
     }
 
-    private IndexRuleBinding(String name, Subject subject, ZonedDateTime updatedAt) {
-        super(name, updatedAt);
-        this.rules = new ArrayList<>();
-        this.subject = subject;
-        this.beginAt = ZonedDateTime.now();
-        this.expireAt = DEFAULT_EXPIRE_AT;
+    @VisibleForTesting
+    static IndexRuleBinding create(String group, String name, Subject subject, List<String> rules, ZonedDateTime beginAt, ZonedDateTime expireAt) {
+        return new AutoValue_IndexRuleBinding(group, name, null,
+                ImmutableList.copyOf(rules), subject, beginAt, expireAt);
     }
 
-    /**
-     * Add a rule name
-     *
-     * @param ruleName the name of the IndexRule in the same group
-     */
-    public IndexRuleBinding addRule(String ruleName) {
-        this.rules.add(ruleName);
-        return this;
+    public static String defaultBindingRule(String entityName) {
+        return entityName + "-index-rule-binding";
     }
 
     @Override
-    public BanyandbMetadata.IndexRuleBinding serialize(String group) {
-        BanyandbMetadata.IndexRuleBinding.Builder b = BanyandbMetadata.IndexRuleBinding.newBuilder()
-                .setMetadata(buildMetadata(group))
-                .addAllRules(this.rules)
-                .setSubject(this.subject.serialize())
-                .setBeginAt(TimeUtils.buildTimestamp(this.beginAt))
-                .setExpireAt(TimeUtils.buildTimestamp(this.expireAt));
-        if (this.updatedAt != null) {
-            b.setUpdatedAt(TimeUtils.buildTimestamp(this.updatedAt));
+    public BanyandbDatabase.IndexRuleBinding serialize() {
+        BanyandbDatabase.IndexRuleBinding.Builder b = BanyandbDatabase.IndexRuleBinding.newBuilder()
+                .setMetadata(buildMetadata())
+                .addAllRules(rules())
+                .setSubject(subject().serialize())
+                .setBeginAt(TimeUtils.buildTimestamp(beginAt()))
+                .setExpireAt(TimeUtils.buildTimestamp(expireAt()));
+        if (updatedAt() != null) {
+            b.setUpdatedAt(TimeUtils.buildTimestamp(updatedAt()));
         }
         return b.build();
     }
 
-    static IndexRuleBinding fromProtobuf(BanyandbMetadata.IndexRuleBinding pb) {
-        IndexRuleBinding indexRuleBinding = new IndexRuleBinding(pb.getMetadata().getName(), Subject.fromProtobuf(pb.getSubject()), TimeUtils.parseTimestamp(pb.getUpdatedAt()));
-        indexRuleBinding.setRules(new ArrayList<>(pb.getRulesList()));
-        indexRuleBinding.setBeginAt(TimeUtils.parseTimestamp(pb.getBeginAt()));
-        indexRuleBinding.setExpireAt(TimeUtils.parseTimestamp(pb.getExpireAt()));
-        return indexRuleBinding;
+    static IndexRuleBinding fromProtobuf(BanyandbDatabase.IndexRuleBinding pb) {
+        return new AutoValue_IndexRuleBinding(
+                pb.getMetadata().getGroup(),
+                pb.getMetadata().getName(),
+                TimeUtils.parseTimestamp(pb.getUpdatedAt()),
+                ImmutableList.copyOf(pb.getRulesList()),
+                Subject.fromProtobuf(pb.getSubject()),
+                TimeUtils.parseTimestamp(pb.getBeginAt()),
+                TimeUtils.parseTimestamp(pb.getExpireAt())
+        );
     }
 
     @RequiredArgsConstructor
     @Getter
     @EqualsAndHashCode
-    public static class Subject implements Serializable<BanyandbMetadata.Subject> {
+    public static class Subject implements Serializable<BanyandbDatabase.Subject> {
         /**
          * name refers to a stream or measure in a particular catalog
          */
@@ -115,8 +112,8 @@ public class IndexRuleBinding extends NamedSchema<BanyandbMetadata.IndexRuleBind
         private final Catalog catalog;
 
         @Override
-        public BanyandbMetadata.Subject serialize() {
-            return BanyandbMetadata.Subject.newBuilder()
+        public BanyandbDatabase.Subject serialize() {
+            return BanyandbDatabase.Subject.newBuilder()
                     .setName(this.name)
                     .setCatalog(this.catalog.getCatalog())
                     .build();
@@ -142,7 +139,7 @@ public class IndexRuleBinding extends NamedSchema<BanyandbMetadata.IndexRuleBind
             return new Subject(name, Catalog.MEASURE);
         }
 
-        private static Subject fromProtobuf(BanyandbMetadata.Subject pb) {
+        private static Subject fromProtobuf(BanyandbDatabase.Subject pb) {
             switch (pb.getCatalog()) {
                 case CATALOG_STREAM:
                     return referToStream(pb.getName());
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBindingMetadataRegistry.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBindingMetadataRegistry.java
index 72eac8b..cbb235a 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBindingMetadataRegistry.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleBindingMetadataRegistry.java
@@ -18,67 +18,62 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import com.google.common.base.Preconditions;
 import io.grpc.Channel;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
-import org.apache.skywalking.banyandb.database.v1.metadata.IndexRuleBindingRegistryServiceGrpc;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.IndexRuleBindingRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.MetadataClient;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
-public class IndexRuleBindingMetadataRegistry extends MetadataClient<BanyandbMetadata.IndexRuleBinding, IndexRuleBinding> {
-    private final IndexRuleBindingRegistryServiceGrpc.IndexRuleBindingRegistryServiceBlockingStub blockingStub;
+public class IndexRuleBindingMetadataRegistry extends MetadataClient<IndexRuleBindingRegistryServiceGrpc.IndexRuleBindingRegistryServiceBlockingStub,
+        BanyandbDatabase.IndexRuleBinding, IndexRuleBinding> {
 
-    public IndexRuleBindingMetadataRegistry(String group, Channel channel) {
-        super(group);
-        Preconditions.checkArgument(channel != null, "channel must not be null");
-        this.blockingStub = IndexRuleBindingRegistryServiceGrpc.newBlockingStub(channel);
+    public IndexRuleBindingMetadataRegistry(Channel channel) {
+        super(IndexRuleBindingRegistryServiceGrpc.newBlockingStub(channel));
     }
 
     @Override
-    public void create(IndexRuleBinding payload) {
-        blockingStub.create(BanyandbMetadata.IndexRuleBindingRegistryServiceCreateRequest.newBuilder()
-                .setIndexRuleBinding(payload.serialize(this.group))
-                .build());
+    public void create(IndexRuleBinding payload) throws BanyanDBException {
+        execute(() -> stub.create(BanyandbDatabase.IndexRuleBindingRegistryServiceCreateRequest.newBuilder()
+                .setIndexRuleBinding(payload.serialize())
+                .build()));
     }
 
     @Override
-    public void update(IndexRuleBinding payload) {
-        blockingStub.update(BanyandbMetadata.IndexRuleBindingRegistryServiceUpdateRequest.newBuilder()
-                .setIndexRuleBinding(payload.serialize(this.group))
-                .build());
+    public void update(IndexRuleBinding payload) throws BanyanDBException {
+        execute(() -> stub.update(BanyandbDatabase.IndexRuleBindingRegistryServiceUpdateRequest.newBuilder()
+                .setIndexRuleBinding(payload.serialize())
+                .build()));
     }
 
     @Override
-    public boolean delete(String name) {
-        BanyandbMetadata.IndexRuleBindingRegistryServiceDeleteResponse resp = blockingStub.delete(BanyandbMetadata.IndexRuleBindingRegistryServiceDeleteRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
+    public boolean delete(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.IndexRuleBindingRegistryServiceDeleteResponse resp = execute(() ->
+                stub.delete(BanyandbDatabase.IndexRuleBindingRegistryServiceDeleteRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
         return resp != null && resp.getDeleted();
     }
 
     @Override
-    public IndexRuleBinding get(String name) {
-        BanyandbMetadata.IndexRuleBindingRegistryServiceGetResponse resp = blockingStub.get(BanyandbMetadata.IndexRuleBindingRegistryServiceGetRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
-        if (resp == null) {
-            return null;
-        }
+    public IndexRuleBinding get(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.IndexRuleBindingRegistryServiceGetResponse resp = execute(() ->
+                stub.get(BanyandbDatabase.IndexRuleBindingRegistryServiceGetRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
 
         return IndexRuleBinding.fromProtobuf(resp.getIndexRuleBinding());
     }
 
     @Override
-    public List<IndexRuleBinding> list() {
-        BanyandbMetadata.IndexRuleBindingRegistryServiceListResponse resp = blockingStub.list(BanyandbMetadata.IndexRuleBindingRegistryServiceListRequest.newBuilder()
-                .setGroup(this.group)
-                .build());
-        if (resp == null) {
-            return Collections.emptyList();
-        }
+    public List<IndexRuleBinding> list(String group) throws BanyanDBException {
+        BanyandbDatabase.IndexRuleBindingRegistryServiceListResponse resp = execute(() ->
+                stub.list(BanyandbDatabase.IndexRuleBindingRegistryServiceListRequest.newBuilder()
+                        .setGroup(group)
+                        .build()));
 
         return resp.getIndexRuleBindingList().stream().map(IndexRuleBinding::fromProtobuf).collect(Collectors.toList());
     }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleMetadataRegistry.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleMetadataRegistry.java
index f97d252..f9ea810 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleMetadataRegistry.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/IndexRuleMetadataRegistry.java
@@ -18,67 +18,63 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import com.google.common.base.Preconditions;
 import io.grpc.Channel;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
-import org.apache.skywalking.banyandb.database.v1.metadata.IndexRuleRegistryServiceGrpc;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.IndexRuleRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.MetadataClient;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
-public class IndexRuleMetadataRegistry extends MetadataClient<BanyandbMetadata.IndexRule, IndexRule> {
-    private final IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceBlockingStub blockingStub;
-
-    public IndexRuleMetadataRegistry(String group, Channel channel) {
-        super(group);
-        Preconditions.checkArgument(channel != null, "channel must not be null");
-        this.blockingStub = IndexRuleRegistryServiceGrpc.newBlockingStub(channel);
+public class IndexRuleMetadataRegistry extends MetadataClient<IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceBlockingStub,
+        BanyandbDatabase.IndexRule, IndexRule> {
+    public IndexRuleMetadataRegistry(Channel channel) {
+        super(IndexRuleRegistryServiceGrpc.newBlockingStub(channel));
     }
 
     @Override
-    public void create(IndexRule payload) {
-        blockingStub.create(BanyandbMetadata.IndexRuleRegistryServiceCreateRequest.newBuilder()
-                .setIndexRule(payload.serialize(this.group))
-                .build());
+    public void create(IndexRule payload) throws BanyanDBException {
+        execute(() ->
+                stub.create(BanyandbDatabase.IndexRuleRegistryServiceCreateRequest.newBuilder()
+                        .setIndexRule(payload.serialize())
+                        .build()));
     }
 
     @Override
-    public void update(IndexRule payload) {
-        blockingStub.update(BanyandbMetadata.IndexRuleRegistryServiceUpdateRequest.newBuilder()
-                .setIndexRule(payload.serialize(this.group))
-                .build());
+    public void update(IndexRule payload) throws BanyanDBException {
+        execute(() ->
+                stub.update(BanyandbDatabase.IndexRuleRegistryServiceUpdateRequest.newBuilder()
+                        .setIndexRule(payload.serialize())
+                        .build()));
     }
 
     @Override
-    public boolean delete(String name) {
-        BanyandbMetadata.IndexRuleRegistryServiceDeleteResponse resp = blockingStub.delete(BanyandbMetadata.IndexRuleRegistryServiceDeleteRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
+    public boolean delete(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.IndexRuleRegistryServiceDeleteResponse resp = execute(() ->
+                stub.delete(BanyandbDatabase.IndexRuleRegistryServiceDeleteRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
         return resp != null && resp.getDeleted();
     }
 
     @Override
-    public IndexRule get(String name) {
-        BanyandbMetadata.IndexRuleRegistryServiceGetResponse resp = blockingStub.get(BanyandbMetadata.IndexRuleRegistryServiceGetRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
-        if (resp == null) {
-            return null;
-        }
+    public IndexRule get(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.IndexRuleRegistryServiceGetResponse resp = execute(() ->
+                stub.get(BanyandbDatabase.IndexRuleRegistryServiceGetRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
 
         return IndexRule.fromProtobuf(resp.getIndexRule());
     }
 
     @Override
-    public List<IndexRule> list() {
-        BanyandbMetadata.IndexRuleRegistryServiceListResponse resp = blockingStub.list(BanyandbMetadata.IndexRuleRegistryServiceListRequest.newBuilder()
-                .setGroup(this.group)
-                .build());
-        if (resp == null) {
-            return Collections.emptyList();
-        }
+    public List<IndexRule> list(String group) throws BanyanDBException {
+        BanyandbDatabase.IndexRuleRegistryServiceListResponse resp = execute(() ->
+                stub.list(BanyandbDatabase.IndexRuleRegistryServiceListRequest.newBuilder()
+                        .setGroup(group)
+                        .build()));
 
         return resp.getIndexRuleList().stream().map(IndexRule::fromProtobuf).collect(Collectors.toList());
     }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Measure.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Measure.java
index 56d7ed8..80b021d 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Measure.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Measure.java
@@ -18,170 +18,152 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import lombok.Setter;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
 import org.apache.skywalking.banyandb.v1.client.util.TimeUtils;
 
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
 
-@Setter
-@Getter
-@EqualsAndHashCode(callSuper = true)
-public class Measure extends NamedSchema<BanyandbMetadata.Measure> {
+@AutoValue
+public abstract class Measure extends NamedSchema<BanyandbDatabase.Measure> {
     /**
      * specs of tag families
      */
-    private List<TagFamilySpec> tagFamilySpecs;
+    abstract ImmutableList<TagFamilySpec> tagFamilies();
 
     /**
      * fieldSpecs denote measure values
      */
-    private List<FieldSpec> fieldSpecs;
+    abstract ImmutableList<FieldSpec> fields();
 
     /**
      * tag names used to generate an entity
      */
-    private List<String> entityTagNames;
+    abstract ImmutableList<String> entityRelativeTags();
 
     /**
-     * intervalRules define data points writing interval
+     * interval indicates how frequently to send a data point
      */
-    private List<IntervalRule<?>> intervalRules;
+    abstract Duration interval();
 
     /**
-     * number of shards
+     * index rules bound to the stream
      */
-    private int shardNum;
+    public abstract ImmutableList<IndexRule> indexRules();
 
-    /**
-     * duration determines how long a Stream keeps its data
-     */
-    private Duration ttl;
+    abstract Measure.Builder toBuilder();
 
-    public Measure(String name, int shardNum, Duration ttl) {
-        this(name, shardNum, ttl, null);
+    public final Measure withIndexRules(List<IndexRule> indexRules) {
+        return toBuilder().addIndexes(indexRules)
+                .build();
     }
 
-    private Measure(String name, int shardNum, Duration ttl, ZonedDateTime updatedAt) {
-        super(name, updatedAt);
-        this.tagFamilySpecs = new ArrayList<>();
-        this.entityTagNames = new ArrayList<>();
-        this.fieldSpecs = new ArrayList<>();
-        this.intervalRules = new ArrayList<>();
-        this.shardNum = shardNum;
-        this.ttl = ttl;
+    public static Measure.Builder create(String group, String name, Duration interval) {
+        return new AutoValue_Measure.Builder().setGroup(group).setName(name).setInterval(interval);
     }
 
-    /**
-     * Add a tag name as a part of the entity
-     *
-     * @param name the name of the tag
-     */
-    public Measure addTagNameAsEntity(String name) {
-        this.entityTagNames.add(name);
-        return this;
-    }
+    @AutoValue.Builder
+    public abstract static class Builder {
+        abstract String group();
 
-    /**
-     * Add a tag family spec to the schema
-     *
-     * @param tagFamilySpec a tag family containing tag specs
-     */
-    public Measure addTagFamilySpec(TagFamilySpec tagFamilySpec) {
-        this.tagFamilySpecs.add(tagFamilySpec);
-        return this;
-    }
+        abstract Measure.Builder setGroup(String group);
 
-    /**
-     * Add an interval rule to the schema
-     *
-     * @param intervalRule an interval rule to match tag name and value
-     */
-    public Measure addIntervalRule(IntervalRule<?> intervalRule) {
-        this.intervalRules.add(intervalRule);
-        return this;
-    }
+        abstract Measure.Builder setName(String name);
 
-    /**
-     * Add a tag family spec to the schema
-     *
-     * @param fieldSpec a tag family containing tag specs
-     */
-    public Measure addFieldSpec(FieldSpec fieldSpec) {
-        this.fieldSpecs.add(fieldSpec);
-        return this;
-    }
+        abstract Measure.Builder setUpdatedAt(ZonedDateTime updatedAt);
 
-    static Measure fromProtobuf(BanyandbMetadata.Measure pb) {
-        Measure m = new Measure(pb.getMetadata().getName(), pb.getOpts().getShardNum(),
-                Duration.fromProtobuf(pb.getOpts().getTtl()),
-                TimeUtils.parseTimestamp(pb.getUpdatedAtNanoseconds()));
+        abstract ImmutableList.Builder<TagFamilySpec> tagFamiliesBuilder();
 
-        // prepare entity
-        for (int i = 0; i < pb.getEntity().getTagNamesCount(); i++) {
-            m.addTagNameAsEntity(pb.getEntity().getTagNames(i));
+        public final Measure.Builder addTagFamily(TagFamilySpec tagFamilySpec) {
+            tagFamiliesBuilder().add(tagFamilySpec);
+            return this;
         }
 
-        // build tag family spec
-        for (int i = 0; i < pb.getTagFamiliesCount(); i++) {
-            m.addTagFamilySpec(TagFamilySpec.fromProtobuf(pb.getTagFamilies(i)));
+        abstract ImmutableList.Builder<FieldSpec> fieldsBuilder();
+
+        public final Measure.Builder addField(FieldSpec fieldSpec) {
+            fieldsBuilder().add(fieldSpec);
+            return this;
+        }
+
+        abstract ImmutableList.Builder<IndexRule> indexRulesBuilder();
+
+        public final Measure.Builder addIndexes(Iterable<IndexRule> indexRules) {
+            indexRulesBuilder().addAll(indexRules);
+            return this;
         }
 
-        // build interval rules
-        for (int i = 0; i < pb.getIntervalRulesCount(); i++) {
-            m.addIntervalRule(IntervalRule.fromProtobuf(pb.getIntervalRules(i)));
+        public final Measure.Builder addIndex(IndexRule indexRule) {
+            indexRulesBuilder().add(indexRule.withGroup(group()));
+            return this;
+        }
+
+        abstract Measure.Builder setInterval(Duration interval);
+
+        public abstract Measure.Builder setEntityRelativeTags(List<String> entityRelativeTags);
+
+        public abstract Measure.Builder setEntityRelativeTags(String... entityRelativeTags);
+
+        public abstract Measure build();
+    }
+
+    static Measure fromProtobuf(BanyandbDatabase.Measure pb) {
+        final Measure.Builder m = Measure.create(pb.getMetadata().getGroup(), pb.getMetadata().getName(),
+                        Duration.parse(pb.getInterval()))
+                .setUpdatedAt(TimeUtils.parseTimestamp(pb.getUpdatedAt()))
+                .setEntityRelativeTags(pb.getEntity().getTagNamesList());
+
+        // build tag family spec
+        for (int i = 0; i < pb.getTagFamiliesCount(); i++) {
+            m.addTagFamily(TagFamilySpec.fromProtobuf(pb.getTagFamilies(i)));
         }
 
         // build field spec
         for (int i = 0; i < pb.getFieldsCount(); i++) {
-            m.addFieldSpec(FieldSpec.fromProtobuf(pb.getFields(i)));
+            m.addField(FieldSpec.fromProtobuf(pb.getFields(i)));
         }
 
-        return m;
+        return m.build();
     }
 
     @Override
-    public BanyandbMetadata.Measure serialize(String group) {
-        List<BanyandbMetadata.TagFamilySpec> tfs = new ArrayList<>(this.tagFamilySpecs.size());
-        for (final TagFamilySpec spec : this.tagFamilySpecs) {
+    public BanyandbDatabase.Measure serialize() {
+        List<BanyandbDatabase.TagFamilySpec> tfs = new ArrayList<>(this.tagFamilies().size());
+        for (final TagFamilySpec spec : this.tagFamilies()) {
             tfs.add(spec.serialize());
         }
 
-        List<BanyandbMetadata.FieldSpec> fs = new ArrayList<>(this.fieldSpecs.size());
-        for (final FieldSpec spec : this.fieldSpecs) {
+        List<BanyandbDatabase.FieldSpec> fs = new ArrayList<>(this.fields().size());
+        for (final FieldSpec spec : this.fields()) {
             fs.add(spec.serialize());
         }
 
-        List<BanyandbMetadata.IntervalRule> irs = new ArrayList<>(this.intervalRules.size());
-        for (final IntervalRule<?> spec : this.intervalRules) {
-            irs.add(spec.serialize());
-        }
-
-        BanyandbMetadata.Measure.Builder b = BanyandbMetadata.Measure.newBuilder()
-                .setMetadata(buildMetadata(group))
+        BanyandbDatabase.Measure.Builder b = BanyandbDatabase.Measure.newBuilder()
+                .setInterval(interval().format())
+                .setMetadata(buildMetadata())
                 .addAllTagFamilies(tfs)
                 .addAllFields(fs)
-                .addAllIntervalRules(irs)
-                .setEntity(BanyandbMetadata.Entity.newBuilder().addAllTagNames(entityTagNames).build())
-                .setOpts(BanyandbMetadata.ResourceOpts.newBuilder().setShardNum(this.shardNum).setTtl(this.ttl.serialize()));
+                .setEntity(BanyandbDatabase.Entity.newBuilder().addAllTagNames(entityRelativeTags()).build());
 
-        if (this.updatedAt != null) {
-            b.setUpdatedAtNanoseconds(TimeUtils.buildTimestamp(this.updatedAt));
+        if (updatedAt() != null) {
+            b.setUpdatedAt(TimeUtils.buildTimestamp(updatedAt()));
         }
 
         return b.build();
     }
 
     @EqualsAndHashCode
-    public static class FieldSpec implements Serializable<BanyandbMetadata.FieldSpec> {
+    public static class FieldSpec implements Serializable<BanyandbDatabase.FieldSpec> {
         /**
          * name is the identity of a field
          */
+        @Getter
         private final String name;
         /**
          * fieldType denotes the type of field value
@@ -205,33 +187,33 @@ public class Measure extends NamedSchema<BanyandbMetadata.Measure> {
 
         @RequiredArgsConstructor
         public enum FieldType {
-            UNSPECIFIED(BanyandbMetadata.FieldType.FIELD_TYPE_UNSPECIFIED),
-            STRING(BanyandbMetadata.FieldType.FIELD_TYPE_STRING),
-            INT(BanyandbMetadata.FieldType.FIELD_TYPE_INT),
-            BINARY(BanyandbMetadata.FieldType.FIELD_TYPE_DATA_BINARY);
+            UNSPECIFIED(BanyandbDatabase.FieldType.FIELD_TYPE_UNSPECIFIED),
+            STRING(BanyandbDatabase.FieldType.FIELD_TYPE_STRING),
+            INT(BanyandbDatabase.FieldType.FIELD_TYPE_INT),
+            BINARY(BanyandbDatabase.FieldType.FIELD_TYPE_DATA_BINARY);
 
-            private final BanyandbMetadata.FieldType fieldType;
+            private final BanyandbDatabase.FieldType fieldType;
         }
 
         @RequiredArgsConstructor
         public enum EncodingMethod {
-            UNSPECIFIED(BanyandbMetadata.EncodingMethod.ENCODING_METHOD_UNSPECIFIED),
-            GORILLA(BanyandbMetadata.EncodingMethod.ENCODING_METHOD_GORILLA);
+            UNSPECIFIED(BanyandbDatabase.EncodingMethod.ENCODING_METHOD_UNSPECIFIED),
+            GORILLA(BanyandbDatabase.EncodingMethod.ENCODING_METHOD_GORILLA);
 
-            private final BanyandbMetadata.EncodingMethod encodingMethod;
+            private final BanyandbDatabase.EncodingMethod encodingMethod;
         }
 
         @RequiredArgsConstructor
         public enum CompressionMethod {
-            UNSPECIFIED(BanyandbMetadata.CompressionMethod.COMPRESSION_METHOD_UNSPECIFIED),
-            ZSTD(BanyandbMetadata.CompressionMethod.COMPRESSION_METHOD_ZSTD);
+            UNSPECIFIED(BanyandbDatabase.CompressionMethod.COMPRESSION_METHOD_UNSPECIFIED),
+            ZSTD(BanyandbDatabase.CompressionMethod.COMPRESSION_METHOD_ZSTD);
 
-            private final BanyandbMetadata.CompressionMethod compressionMethod;
+            private final BanyandbDatabase.CompressionMethod compressionMethod;
         }
 
         @Override
-        public BanyandbMetadata.FieldSpec serialize() {
-            return BanyandbMetadata.FieldSpec.newBuilder()
+        public BanyandbDatabase.FieldSpec serialize() {
+            return BanyandbDatabase.FieldSpec.newBuilder()
                     .setName(this.name)
                     .setFieldType(this.fieldType.fieldType)
                     .setEncodingMethod(this.encodingMethod.encodingMethod)
@@ -239,7 +221,7 @@ public class Measure extends NamedSchema<BanyandbMetadata.Measure> {
                     .build();
         }
 
-        private static FieldSpec fromProtobuf(BanyandbMetadata.FieldSpec pb) {
+        private static FieldSpec fromProtobuf(BanyandbDatabase.FieldSpec pb) {
             Builder b = null;
             switch (pb.getFieldType()) {
                 case FIELD_TYPE_STRING:
@@ -332,82 +314,4 @@ public class Measure extends NamedSchema<BanyandbMetadata.Measure> {
             }
         }
     }
-
-    @Getter
-    @EqualsAndHashCode
-    public static abstract class IntervalRule<T> implements Serializable<BanyandbMetadata.IntervalRule> {
-        /**
-         * name of the tag to be matched
-         */
-        protected final String tagName;
-        /**
-         * value of the tag matched
-         */
-        protected final T tagValue;
-        /**
-         * interval of the measure
-         */
-        protected final String interval;
-
-        public IntervalRule(String tagName, T tagValue, String interval) {
-            this.tagName = tagName;
-            this.tagValue = tagValue;
-            this.interval = interval;
-        }
-
-        protected abstract BanyandbMetadata.IntervalRule.Builder applyTagValue(BanyandbMetadata.IntervalRule.Builder b);
-
-        /**
-         * Create an interval rule to match a tag with string value
-         *
-         * @param tagName  name of the tag
-         * @param tagValue value of the tag, which must be string
-         * @param interval interval of the data point
-         * @return an interval rule to match a tag with given string
-         */
-        public static IntervalRule<String> matchStringLabel(final String tagName, final String tagValue, final String interval) {
-            return new IntervalRule<String>(tagName, tagValue, interval) {
-                @Override
-                protected BanyandbMetadata.IntervalRule.Builder applyTagValue(BanyandbMetadata.IntervalRule.Builder b) {
-                    return b.setStr(this.tagValue);
-                }
-            };
-        }
-
-        /**
-         * Create an interval rule to match a tag with string value
-         *
-         * @param tagName  name of the tag
-         * @param tagValue value of the tag, which must be string
-         * @param interval interval of the data point
-         * @return an interval rule to match a tag with given string
-         */
-        public static IntervalRule<Long> matchNumericLabel(final String tagName, final Long tagValue, final String interval) {
-            return new IntervalRule<Long>(tagName, tagValue, interval) {
-                @Override
-                protected BanyandbMetadata.IntervalRule.Builder applyTagValue(BanyandbMetadata.IntervalRule.Builder b) {
-                    return b.setInt(this.tagValue);
-                }
-            };
-        }
-
-        @Override
-        public BanyandbMetadata.IntervalRule serialize() {
-            return applyTagValue(BanyandbMetadata.IntervalRule.newBuilder()
-                    .setTagName(this.tagName)
-                    .setInterval(this.interval))
-                    .build();
-        }
-
-        private static IntervalRule<?> fromProtobuf(BanyandbMetadata.IntervalRule pb) {
-            switch (pb.getTagValueCase()) {
-                case STR:
-                    return matchStringLabel(pb.getTagName(), pb.getStr(), pb.getInterval());
-                case INT:
-                    return matchNumericLabel(pb.getTagName(), pb.getInt(), pb.getInterval());
-                default:
-                    throw new IllegalArgumentException("unrecognized tag value type");
-            }
-        }
-    }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MeasureMetadataRegistry.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MeasureMetadataRegistry.java
index f3ecbac..c3f990d 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MeasureMetadataRegistry.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MeasureMetadataRegistry.java
@@ -18,67 +18,64 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import com.google.common.base.Preconditions;
 import io.grpc.Channel;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
-import org.apache.skywalking.banyandb.database.v1.metadata.MeasureRegistryServiceGrpc;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.MeasureRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.MetadataClient;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
-public class MeasureMetadataRegistry extends MetadataClient<BanyandbMetadata.Measure, Measure> {
-    private final MeasureRegistryServiceGrpc.MeasureRegistryServiceBlockingStub blockingStub;
+public class MeasureMetadataRegistry extends MetadataClient<MeasureRegistryServiceGrpc.MeasureRegistryServiceBlockingStub,
+        BanyandbDatabase.Measure, Measure> {
 
-    public MeasureMetadataRegistry(String group, Channel channel) {
-        super(group);
-        Preconditions.checkArgument(channel != null, "channel must not be null");
-        this.blockingStub = MeasureRegistryServiceGrpc.newBlockingStub(channel);
+    public MeasureMetadataRegistry(Channel channel) {
+        super(MeasureRegistryServiceGrpc.newBlockingStub(channel));
     }
 
     @Override
-    public void create(Measure payload) {
-        blockingStub.create(BanyandbMetadata.MeasureRegistryServiceCreateRequest.newBuilder()
-                .setMeasure(payload.serialize(this.group))
-                .build());
+    public void create(final Measure payload) throws BanyanDBException {
+        execute(() ->
+                stub.create(BanyandbDatabase.MeasureRegistryServiceCreateRequest.newBuilder()
+                        .setMeasure(payload.serialize())
+                        .build()));
     }
 
     @Override
-    public void update(Measure payload) {
-        blockingStub.update(BanyandbMetadata.MeasureRegistryServiceUpdateRequest.newBuilder()
-                .setMeasure(payload.serialize(this.group))
-                .build());
+    public void update(final Measure payload) throws BanyanDBException {
+        execute(() ->
+                stub.update(BanyandbDatabase.MeasureRegistryServiceUpdateRequest.newBuilder()
+                        .setMeasure(payload.serialize())
+                        .build()));
     }
 
     @Override
-    public boolean delete(String name) {
-        BanyandbMetadata.MeasureRegistryServiceDeleteResponse resp = blockingStub.delete(BanyandbMetadata.MeasureRegistryServiceDeleteRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
+    public boolean delete(final String group, final String name) throws BanyanDBException {
+        BanyandbDatabase.MeasureRegistryServiceDeleteResponse resp = execute(() ->
+                stub.delete(BanyandbDatabase.MeasureRegistryServiceDeleteRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
         return resp != null && resp.getDeleted();
     }
 
     @Override
-    public Measure get(String name) {
-        BanyandbMetadata.MeasureRegistryServiceGetResponse resp = blockingStub.get(BanyandbMetadata.MeasureRegistryServiceGetRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
-        if (resp == null) {
-            return null;
-        }
+    public Measure get(final String group, final String name) throws BanyanDBException {
+        BanyandbDatabase.MeasureRegistryServiceGetResponse resp = execute(() ->
+                stub.get(BanyandbDatabase.MeasureRegistryServiceGetRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
 
         return Measure.fromProtobuf(resp.getMeasure());
     }
 
     @Override
-    public List<Measure> list() {
-        BanyandbMetadata.MeasureRegistryServiceListResponse resp = blockingStub.list(BanyandbMetadata.MeasureRegistryServiceListRequest.newBuilder()
-                .setGroup(this.group)
-                .build());
-        if (resp == null) {
-            return Collections.emptyList();
-        }
+    public List<Measure> list(final String group) throws BanyanDBException {
+        BanyandbDatabase.MeasureRegistryServiceListResponse resp = execute(() ->
+                stub.list(BanyandbDatabase.MeasureRegistryServiceListRequest.newBuilder()
+                        .setGroup(group)
+                        .build()));
 
         return resp.getMeasureList().stream().map(Measure::fromProtobuf).collect(Collectors.toList());
     }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MetadataCache.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MetadataCache.java
new file mode 100644
index 0000000..f853513
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MetadataCache.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.metadata;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.apache.skywalking.banyandb.v1.client.util.CopyOnWriteMap;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public enum MetadataCache {
+    INSTANCE;
+
+    private final Map<String, EntityMetadata> cache;
+
+    MetadataCache() {
+        this.cache = new CopyOnWriteMap<>();
+    }
+
+    public Stream register(Stream stream) {
+        this.cache.put(formatKey(stream.group(), stream.name()), parse(stream));
+        return stream;
+    }
+
+    public Measure register(Measure measure) {
+        this.cache.put(formatKey(measure.group(), measure.name()), parse(measure));
+        return measure;
+    }
+
+    public EntityMetadata findMetadata(String group, String name) {
+        return this.cache.get(formatKey(group, name));
+    }
+
+    static String formatKey(String group, String name) {
+        return group + ":" + name;
+    }
+
+    static EntityMetadata parse(Stream s) {
+        int totalTags = 0;
+        final int[] tagFamilyCapacity = new int[s.tagFamilies().size()];
+        Map<String, TagInfo> tagInfo = new HashMap<>();
+        int k = 0;
+        for (int i = 0; i < s.tagFamilies().size(); i++) {
+            final String tagFamilyName = s.tagFamilies().get(i).tagFamilyName();
+            tagFamilyCapacity[i] = s.tagFamilies().get(i).tagSpecs().size();
+            totalTags += tagFamilyCapacity[i];
+            for (int j = 0; j < tagFamilyCapacity[i]; j++) {
+                tagInfo.put(s.tagFamilies().get(i).tagSpecs().get(j).getTagName(), new TagInfo(tagFamilyName, k++));
+            }
+        }
+        return new EntityMetadata(totalTags, 0, tagFamilyCapacity,
+                Collections.unmodifiableMap(tagInfo),
+                Collections.emptyMap());
+    }
+
+    static EntityMetadata parse(Measure m) {
+        int totalTags = 0;
+        final int[] tagFamilyCapacity = new int[m.tagFamilies().size()];
+        final Map<String, TagInfo> tagOffset = new HashMap<>();
+        int k = 0;
+        for (int i = 0; i < m.tagFamilies().size(); i++) {
+            final String tagFamilyName = m.tagFamilies().get(i).tagFamilyName();
+            tagFamilyCapacity[i] = m.tagFamilies().get(i).tagSpecs().size();
+            totalTags += tagFamilyCapacity[i];
+            for (int j = 0; j < tagFamilyCapacity[i]; j++) {
+                tagOffset.put(m.tagFamilies().get(i).tagSpecs().get(j).getTagName(), new TagInfo(tagFamilyName, k++));
+            }
+        }
+        final Map<String, Integer> fieldOffset = new HashMap<>();
+        for (int i = 0; i < m.fields().size(); i++) {
+            fieldOffset.put(m.fields().get(i).getName(), i);
+        }
+        return new EntityMetadata(totalTags, m.fields().size(), tagFamilyCapacity,
+                Collections.unmodifiableMap(tagOffset), Collections.unmodifiableMap(fieldOffset));
+    }
+
+    @Getter
+    @RequiredArgsConstructor
+    public static class EntityMetadata {
+        private final int totalTags;
+
+        private final int totalFields;
+
+        private final int[] tagFamilyCapacity;
+
+        private final Map<String, TagInfo> tagOffset;
+
+        private final Map<String, Integer> fieldOffset;
+
+        public Optional<TagInfo> findTagInfo(String name) {
+            return Optional.ofNullable(this.tagOffset.get(name));
+        }
+
+        public int findFieldInfo(String name) {
+            return this.fieldOffset.get(name);
+        }
+    }
+
+    @RequiredArgsConstructor
+    @Getter
+    public static class TagInfo {
+        private final String tagFamilyName;
+        private final int offset;
+    }
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MetadataClient.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MetadataClient.java
deleted file mode 100644
index 31932e3..0000000
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/MetadataClient.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.apache.skywalking.banyandb.v1.client.metadata;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.protobuf.GeneratedMessageV3;
-
-import java.util.List;
-
-/**
- * abstract metadata client which defines CRUD operations for a specific kind of schema.
- *
- * @param <P> ProtoBuf: schema defined in ProtoBuf format
- * @param <S> NamedSchema: Java implementation (POJO) which can be serialized to P
- */
-public abstract class MetadataClient<P extends GeneratedMessageV3, S extends NamedSchema<P>> {
-    protected final String group;
-
-    protected MetadataClient(String group) {
-        Preconditions.checkArgument(!Strings.isNullOrEmpty(group), "group must not be null or empty");
-        this.group = group;
-    }
-
-    /**
-     * Create a schema
-     *
-     * @param payload the schema to be created
-     */
-    abstract void create(S payload);
-
-    /**
-     * Update the schema
-     *
-     * @param payload the schema which will be updated with the given name and group
-     */
-    abstract void update(S payload);
-
-    /**
-     * Delete a schema
-     *
-     * @param name the name of the schema to be removed
-     * @return whether this schema is deleted
-     */
-    abstract boolean delete(String name);
-
-    /**
-     * Get a schema with name
-     *
-     * @param name the name of the schema to be found
-     * @return the schema, null if not found
-     */
-    abstract S get(String name);
-
-    /**
-     * List all schemas with the same group name
-     *
-     * @return a list of schemas found
-     */
-    abstract List<S> list();
-}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/NamedSchema.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/NamedSchema.java
index d200962..660eea0 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/NamedSchema.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/NamedSchema.java
@@ -18,11 +18,12 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
+import com.google.common.base.Strings;
 import com.google.protobuf.GeneratedMessageV3;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.v1.client.util.IgnoreHashEquals;
 
+import javax.annotation.Nullable;
 import java.time.ZonedDateTime;
 
 /**
@@ -30,29 +31,33 @@ import java.time.ZonedDateTime;
  *
  * @param <P> In BanyanDB, we have Stream, IndexRule, IndexRuleBinding and Measure
  */
-@Getter
-@EqualsAndHashCode
 public abstract class NamedSchema<P extends GeneratedMessageV3> {
+    /**
+     * group of the NamedSchema
+     */
+    @Nullable
+    public abstract String group();
+
     /**
      * name of the NamedSchema
      */
-    protected final String name;
+    public abstract String name();
 
     /**
      * last updated timestamp
      * This field can only be set by the server.
      */
-    @EqualsAndHashCode.Exclude
-    protected final ZonedDateTime updatedAt;
-
-    protected NamedSchema(String name, ZonedDateTime updatedAt) {
-        this.name = name;
-        this.updatedAt = updatedAt;
-    }
-
-    public abstract P serialize(String group);
-
-    protected Banyandb.Metadata buildMetadata(String group) {
-        return Banyandb.Metadata.newBuilder().setName(this.name).setGroup(group).build();
+    @Nullable
+    @IgnoreHashEquals
+    abstract ZonedDateTime updatedAt();
+
+    public abstract P serialize();
+
+    protected BanyandbCommon.Metadata buildMetadata() {
+        if (Strings.isNullOrEmpty(group())) {
+            return BanyandbCommon.Metadata.newBuilder().setName(name()).build();
+        } else {
+            return BanyandbCommon.Metadata.newBuilder().setName(name()).setGroup(group()).build();
+        }
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Stream.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Stream.java
index 1a17153..9bf62c1 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Stream.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Stream.java
@@ -18,102 +18,104 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.Setter;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
 import org.apache.skywalking.banyandb.v1.client.util.TimeUtils;
 
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
 
-@Setter
-@Getter
-@EqualsAndHashCode(callSuper = true)
-public class Stream extends NamedSchema<BanyandbMetadata.Stream> {
+@AutoValue
+public abstract class Stream extends NamedSchema<BanyandbDatabase.Stream> {
     /**
      * specs of tag families
      */
-    private List<TagFamilySpec> tagFamilySpecs;
+    abstract ImmutableList<TagFamilySpec> tagFamilies();
 
     /**
      * tag names used to generate an entity
      */
-    private List<String> entityTagNames;
+    abstract ImmutableList<String> entityRelativeTags();
 
     /**
-     * number of shards
+     * index rules bound to the stream
      */
-    private int shardNum;
+    public abstract ImmutableList<IndexRule> indexRules();
 
-    /**
-     * duration determines how long a Stream keeps its data
-     */
-    private Duration ttl;
+    abstract Builder toBuilder();
 
-    public Stream(String name, int shardNum, Duration ttl) {
-        this(name, shardNum, ttl, null);
+    public final Stream withIndexRules(List<IndexRule> indexRules) {
+        return toBuilder().addIndexes(indexRules).build();
     }
 
-    private Stream(String name, int shardNum, Duration ttl, ZonedDateTime updatedAt) {
-        super(name, updatedAt);
-        this.tagFamilySpecs = new ArrayList<>(2);
-        this.entityTagNames = new ArrayList<>();
-        this.shardNum = shardNum;
-        this.ttl = ttl;
+    public static Stream.Builder create(String group, String name) {
+        return new AutoValue_Stream.Builder().setGroup(group).setName(name);
     }
 
-    /**
-     * Add a tag name as a part of the entity
-     *
-     * @param name the name of the tag
-     */
-    public Stream addTagNameAsEntity(String name) {
-        this.entityTagNames.add(name);
-        return this;
-    }
+    @AutoValue.Builder
+    public abstract static class Builder {
+        abstract String group();
 
-    /**
-     * Add a tag family spec to the schema
-     *
-     * @param tagFamilySpec a tag family containing tag specs
-     */
-    public Stream addTagFamilySpec(TagFamilySpec tagFamilySpec) {
-        this.tagFamilySpecs.add(tagFamilySpec);
-        return this;
+        abstract Builder setGroup(String group);
+
+        abstract Builder setName(String name);
+
+        abstract Builder setUpdatedAt(ZonedDateTime updatedAt);
+
+        abstract ImmutableList.Builder<TagFamilySpec> tagFamiliesBuilder();
+
+        public final Builder addTagFamily(TagFamilySpec tagFamilySpec) {
+            tagFamiliesBuilder().add(tagFamilySpec);
+            return this;
+        }
+
+        abstract ImmutableList.Builder<IndexRule> indexRulesBuilder();
+
+        public final Builder addIndexes(Iterable<IndexRule> indexRules) {
+            indexRulesBuilder().addAll(indexRules);
+            return this;
+        }
+
+        public final Builder addIndex(IndexRule indexRule) {
+            indexRulesBuilder().add(indexRule.withGroup(group()));
+            return this;
+        }
+
+        public abstract Builder setEntityRelativeTags(String... entityRelativeTags);
+
+        public abstract Builder setEntityRelativeTags(List<String> entityRelativeTags);
+
+        public abstract Stream build();
     }
 
     @Override
-    public BanyandbMetadata.Stream serialize(String group) {
-        List<BanyandbMetadata.TagFamilySpec> metadataTagFamilySpecs = new ArrayList<>(this.tagFamilySpecs.size());
-        for (final TagFamilySpec spec : this.tagFamilySpecs) {
+    public BanyandbDatabase.Stream serialize() {
+        List<BanyandbDatabase.TagFamilySpec> metadataTagFamilySpecs = new ArrayList<>(this.tagFamilies().size());
+        for (final TagFamilySpec spec : this.tagFamilies()) {
             metadataTagFamilySpecs.add(spec.serialize());
         }
 
-        BanyandbMetadata.Stream.Builder b = BanyandbMetadata.Stream.newBuilder()
-                .setMetadata(buildMetadata(group))
+        BanyandbDatabase.Stream.Builder b = BanyandbDatabase.Stream.newBuilder()
+                .setMetadata(buildMetadata())
                 .addAllTagFamilies(metadataTagFamilySpecs)
-                .setEntity(BanyandbMetadata.Entity.newBuilder().addAllTagNames(entityTagNames).build())
-                .setOpts(BanyandbMetadata.ResourceOpts.newBuilder().setShardNum(this.shardNum).setTtl(this.ttl.serialize()));
+                .setEntity(BanyandbDatabase.Entity.newBuilder().addAllTagNames(entityRelativeTags()).build());
 
-        if (this.updatedAt != null) {
-            b.setUpdatedAtNanoseconds(TimeUtils.buildTimestamp(this.updatedAt));
+        if (this.updatedAt() != null) {
+            b.setUpdatedAt(TimeUtils.buildTimestamp(updatedAt()));
         }
         return b.build();
     }
 
-    static Stream fromProtobuf(final BanyandbMetadata.Stream pb) {
-        Stream s = new Stream(pb.getMetadata().getName(), pb.getOpts().getShardNum(),
-                Duration.fromProtobuf(pb.getOpts().getTtl()), TimeUtils.parseTimestamp(pb.getUpdatedAtNanoseconds()));
-        // prepare entity
-        for (int i = 0; i < pb.getEntity().getTagNamesCount(); i++) {
-            s.addTagNameAsEntity(pb.getEntity().getTagNames(i));
-        }
+    public static Stream fromProtobuf(final BanyandbDatabase.Stream pb) {
+        Stream.Builder s = Stream.create(pb.getMetadata().getGroup(), pb.getMetadata().getName())
+                .setUpdatedAt(TimeUtils.parseTimestamp(pb.getUpdatedAt()))
+                .setEntityRelativeTags(pb.getEntity().getTagNamesList());
         // build tag family spec
         for (int i = 0; i < pb.getTagFamiliesCount(); i++) {
-            s.addTagFamilySpec(TagFamilySpec.fromProtobuf(pb.getTagFamilies(i)));
+            s.addTagFamily(TagFamilySpec.fromProtobuf(pb.getTagFamilies(i)));
         }
-        return s;
+        return s.build();
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/StreamMetadataRegistry.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/StreamMetadataRegistry.java
index f39b063..d7da85a 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/StreamMetadataRegistry.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/StreamMetadataRegistry.java
@@ -18,67 +18,64 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
-import com.google.common.base.Preconditions;
 import io.grpc.Channel;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
-import org.apache.skywalking.banyandb.database.v1.metadata.StreamRegistryServiceGrpc;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.StreamRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.MetadataClient;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
-public class StreamMetadataRegistry extends MetadataClient<BanyandbMetadata.Stream, Stream> {
-    private final StreamRegistryServiceGrpc.StreamRegistryServiceBlockingStub blockingStub;
+public class StreamMetadataRegistry extends MetadataClient<StreamRegistryServiceGrpc.StreamRegistryServiceBlockingStub,
+        BanyandbDatabase.Stream, Stream> {
 
-    public StreamMetadataRegistry(String group, Channel channel) {
-        super(group);
-        Preconditions.checkArgument(channel != null, "channel must not be null");
-        this.blockingStub = StreamRegistryServiceGrpc.newBlockingStub(channel);
+    public StreamMetadataRegistry(Channel channel) {
+        super(StreamRegistryServiceGrpc.newBlockingStub(channel));
     }
 
     @Override
-    public void create(Stream payload) {
-        blockingStub.create(BanyandbMetadata.StreamRegistryServiceCreateRequest.newBuilder()
-                .setStream(payload.serialize(this.group))
-                .build());
+    public void create(Stream payload) throws BanyanDBException {
+        execute(() ->
+                stub.create(BanyandbDatabase.StreamRegistryServiceCreateRequest.newBuilder()
+                        .setStream(payload.serialize())
+                        .build()));
     }
 
     @Override
-    public void update(Stream payload) {
-        blockingStub.update(BanyandbMetadata.StreamRegistryServiceUpdateRequest.newBuilder()
-                .setStream(payload.serialize(this.group))
-                .build());
+    public void update(Stream payload) throws BanyanDBException {
+        execute(() ->
+                stub.update(BanyandbDatabase.StreamRegistryServiceUpdateRequest.newBuilder()
+                        .setStream(payload.serialize())
+                        .build()));
     }
 
     @Override
-    public boolean delete(String name) {
-        BanyandbMetadata.StreamRegistryServiceDeleteResponse resp = blockingStub.delete(BanyandbMetadata.StreamRegistryServiceDeleteRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
+    public boolean delete(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.StreamRegistryServiceDeleteResponse resp = execute(() ->
+                stub.delete(BanyandbDatabase.StreamRegistryServiceDeleteRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
         return resp != null && resp.getDeleted();
     }
 
     @Override
-    public Stream get(String name) {
-        BanyandbMetadata.StreamRegistryServiceGetResponse resp = blockingStub.get(BanyandbMetadata.StreamRegistryServiceGetRequest.newBuilder()
-                .setMetadata(Banyandb.Metadata.newBuilder().setGroup(this.group).setName(name).build())
-                .build());
-        if (resp == null) {
-            return null;
-        }
+    public Stream get(String group, String name) throws BanyanDBException {
+        BanyandbDatabase.StreamRegistryServiceGetResponse resp = execute(() ->
+                stub.get(BanyandbDatabase.StreamRegistryServiceGetRequest.newBuilder()
+                        .setMetadata(BanyandbCommon.Metadata.newBuilder().setGroup(group).setName(name).build())
+                        .build()));
 
         return Stream.fromProtobuf(resp.getStream());
     }
 
     @Override
-    public List<Stream> list() {
-        BanyandbMetadata.StreamRegistryServiceListResponse resp = blockingStub.list(BanyandbMetadata.StreamRegistryServiceListRequest.newBuilder()
-                .setGroup(this.group)
-                .build());
-        if (resp == null) {
-            return Collections.emptyList();
-        }
+    public List<Stream> list(String group) throws BanyanDBException {
+        BanyandbDatabase.StreamRegistryServiceListResponse resp = execute(() ->
+                stub.list(BanyandbDatabase.StreamRegistryServiceListRequest.newBuilder()
+                        .setGroup(group)
+                        .build()));
 
         return resp.getStreamList().stream().map(Stream::fromProtobuf).collect(Collectors.toList());
     }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/TagFamilySpec.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/TagFamilySpec.java
index 250e58a..ff14425 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/TagFamilySpec.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/TagFamilySpec.java
@@ -18,93 +18,95 @@
 
 package org.apache.skywalking.banyandb.v1.client.metadata;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import lombok.AccessLevel;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.database.v1.metadata.BanyandbMetadata;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
 
 import java.util.ArrayList;
 import java.util.List;
 
-@Getter
-@EqualsAndHashCode
-public class TagFamilySpec implements Serializable<BanyandbMetadata.TagFamilySpec> {
+@AutoValue
+public abstract class TagFamilySpec implements Serializable<BanyandbDatabase.TagFamilySpec> {
     /**
      * name of the tag family
      */
-    private final String tagFamilyName;
+    abstract String tagFamilyName();
 
     /**
      * TagSpec(s) contained in this family
      */
-    private final List<TagSpec> tagSpecs;
+    abstract ImmutableList<TagSpec> tagSpecs();
 
-    public TagFamilySpec(String tagFamilyName) {
-        this(tagFamilyName, new ArrayList<>(5));
+    public static TagFamilySpec.Builder create(String tagFamilyName) {
+        return new AutoValue_TagFamilySpec.Builder().setTagFamilyName(tagFamilyName);
     }
 
-    public TagFamilySpec(String tagFamilyName, List<TagSpec> specs) {
-        Preconditions.checkArgument(specs != null, "spec must not be null");
-        this.tagFamilyName = tagFamilyName;
-        this.tagSpecs = specs;
-    }
+    @AutoValue.Builder
+    public abstract static class Builder {
+        abstract Builder setTagFamilyName(String tagFamilyName);
 
-    /**
-     * Add a tag spec to this family
-     *
-     * @param tagSpec the tag spec to be appended
-     */
-    public TagFamilySpec addTagSpec(TagSpec tagSpec) {
-        this.tagSpecs.add(tagSpec);
-        return this;
+        abstract ImmutableList.Builder<TagSpec> tagSpecsBuilder();
+
+        public final Builder addTagSpec(TagSpec tagSpec) {
+            tagSpecsBuilder().add(tagSpec);
+            return this;
+        }
+
+        public abstract TagFamilySpec build();
     }
 
-    public BanyandbMetadata.TagFamilySpec serialize() {
-        List<BanyandbMetadata.TagSpec> metadataTagSpecs = new ArrayList<>(this.tagSpecs.size());
-        for (final TagSpec spec : this.tagSpecs) {
+    public BanyandbDatabase.TagFamilySpec serialize() {
+        List<BanyandbDatabase.TagSpec> metadataTagSpecs = new ArrayList<>(this.tagSpecs().size());
+        for (final TagSpec spec : this.tagSpecs()) {
             metadataTagSpecs.add(spec.serialize());
         }
-        return BanyandbMetadata.TagFamilySpec.newBuilder()
-                .setName(this.tagFamilyName)
+        return BanyandbDatabase.TagFamilySpec.newBuilder()
+                .setName(tagFamilyName())
                 .addAllTags(metadataTagSpecs)
                 .build();
     }
 
-    static TagFamilySpec fromProtobuf(BanyandbMetadata.TagFamilySpec pb) {
-        final TagFamilySpec tagFamilySpec = new TagFamilySpec(pb.getName());
+    public static TagFamilySpec fromProtobuf(BanyandbDatabase.TagFamilySpec pb) {
+        final TagFamilySpec.Builder builder = TagFamilySpec.create(pb.getName());
         for (int j = 0; j < pb.getTagsCount(); j++) {
-            final BanyandbMetadata.TagSpec ts = pb.getTags(j);
+            final BanyandbDatabase.TagSpec ts = pb.getTags(j);
             final String tagName = ts.getName();
             switch (ts.getType()) {
                 case TAG_TYPE_INT:
-                    tagFamilySpec.addTagSpec(TagFamilySpec.TagSpec.newIntTag(tagName));
+                    builder.addTagSpec(TagFamilySpec.TagSpec.newIntTag(tagName));
                     break;
                 case TAG_TYPE_STRING:
-                    tagFamilySpec.addTagSpec(TagFamilySpec.TagSpec.newStringTag(tagName));
+                    builder.addTagSpec(TagFamilySpec.TagSpec.newStringTag(tagName));
                     break;
                 case TAG_TYPE_INT_ARRAY:
-                    tagFamilySpec.addTagSpec(TagFamilySpec.TagSpec.newIntArrayTag(tagName));
+                    builder.addTagSpec(TagFamilySpec.TagSpec.newIntArrayTag(tagName));
                     break;
                 case TAG_TYPE_STRING_ARRAY:
-                    tagFamilySpec.addTagSpec(TagFamilySpec.TagSpec.newStringArrayTag(tagName));
+                    builder.addTagSpec(TagFamilySpec.TagSpec.newStringArrayTag(tagName));
                     break;
                 case TAG_TYPE_DATA_BINARY:
-                    tagFamilySpec.addTagSpec(TagFamilySpec.TagSpec.newBinaryTag(tagName));
+                    builder.addTagSpec(TagFamilySpec.TagSpec.newBinaryTag(tagName));
+                    break;
+                case TAG_TYPE_ID:
+                    builder.addTagSpec(TagFamilySpec.TagSpec.newIDTag(tagName));
                     break;
                 default:
                     throw new IllegalStateException("unrecognized tag type");
             }
         }
 
-        return tagFamilySpec;
+        return builder.build();
     }
 
     @Getter
     @EqualsAndHashCode
-    public static class TagSpec implements Serializable<BanyandbMetadata.TagSpec> {
+    public static class TagSpec implements Serializable<BanyandbDatabase.TagSpec> {
         /**
          * name of the tag
          */
@@ -170,9 +172,19 @@ public class TagFamilySpec implements Serializable<BanyandbMetadata.TagFamilySpe
             return new TagSpec(name, TagType.BINARY);
         }
 
+        /**
+         * create a ID tag spec
+         *
+         * @param name the name of the tag
+         * @return a binary array tag spec
+         */
+        public static TagSpec newIDTag(final String name) {
+            return new TagSpec(name, TagType.ID);
+        }
+
         @Override
-        public BanyandbMetadata.TagSpec serialize() {
-            return BanyandbMetadata.TagSpec.newBuilder()
+        public BanyandbDatabase.TagSpec serialize() {
+            return BanyandbDatabase.TagSpec.newBuilder()
                     .setName(this.tagName)
                     .setType(this.tagType.getTagType())
                     .build();
@@ -180,14 +192,15 @@ public class TagFamilySpec implements Serializable<BanyandbMetadata.TagFamilySpe
 
         @RequiredArgsConstructor
         public enum TagType {
-            INT(BanyandbMetadata.TagType.TAG_TYPE_INT),
-            STRING(BanyandbMetadata.TagType.TAG_TYPE_STRING),
-            INT_ARRAY(BanyandbMetadata.TagType.TAG_TYPE_INT_ARRAY),
-            STRING_ARRAY(BanyandbMetadata.TagType.TAG_TYPE_STRING_ARRAY),
-            BINARY(BanyandbMetadata.TagType.TAG_TYPE_DATA_BINARY);
+            INT(BanyandbDatabase.TagType.TAG_TYPE_INT),
+            STRING(BanyandbDatabase.TagType.TAG_TYPE_STRING),
+            INT_ARRAY(BanyandbDatabase.TagType.TAG_TYPE_INT_ARRAY),
+            STRING_ARRAY(BanyandbDatabase.TagType.TAG_TYPE_STRING_ARRAY),
+            BINARY(BanyandbDatabase.TagType.TAG_TYPE_DATA_BINARY),
+            ID(BanyandbDatabase.TagType.TAG_TYPE_ID);
 
             @Getter(AccessLevel.PRIVATE)
-            private final BanyandbMetadata.TagType tagType;
+            private final BanyandbDatabase.TagType tagType;
         }
     }
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/util/CopyOnWriteMap.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/util/CopyOnWriteMap.java
new file mode 100644
index 0000000..c4d9da8
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/util/CopyOnWriteMap.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ * Copyright 2015 Confluent Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Original license:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package org.apache.skywalking.banyandb.v1.client.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A simple read-optimized map implementation that synchronizes only writes and does a full copy on
+ * each modification
+ */
+public class CopyOnWriteMap<K, V> implements ConcurrentMap<K, V> {
+
+    private volatile Map<K, V> map;
+
+    public CopyOnWriteMap() {
+        this.map = Collections.emptyMap();
+    }
+
+    public CopyOnWriteMap(Map<K, V> map) {
+        this.map = Collections.unmodifiableMap(map);
+    }
+
+    @Override
+    public boolean containsKey(Object k) {
+        return map.containsKey(k);
+    }
+
+    @Override
+    public boolean containsValue(Object v) {
+        return map.containsValue(v);
+    }
+
+    @Override
+    public Set<java.util.Map.Entry<K, V>> entrySet() {
+        return map.entrySet();
+    }
+
+    @Override
+    public V get(Object k) {
+        return map.get(k);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return map.isEmpty();
+    }
+
+    @Override
+    public Set<K> keySet() {
+        return map.keySet();
+    }
+
+    @Override
+    public int size() {
+        return map.size();
+    }
+
+    @Override
+    public Collection<V> values() {
+        return map.values();
+    }
+
+    @Override
+    public synchronized void clear() {
+        this.map = Collections.emptyMap();
+    }
+
+    @Override
+    public synchronized V put(K k, V v) {
+        Map<K, V> copy = new HashMap<K, V>(this.map);
+        V prev = copy.put(k, v);
+        this.map = Collections.unmodifiableMap(copy);
+        return prev;
+    }
+
+    @Override
+    public synchronized void putAll(Map<? extends K, ? extends V> entries) {
+        Map<K, V> copy = new HashMap<K, V>(this.map);
+        copy.putAll(entries);
+        this.map = Collections.unmodifiableMap(copy);
+    }
+
+    @Override
+    public synchronized V remove(Object key) {
+        Map<K, V> copy = new HashMap<K, V>(this.map);
+        V prev = copy.remove(key);
+        this.map = Collections.unmodifiableMap(copy);
+        return prev;
+    }
+
+    @Override
+    public synchronized V putIfAbsent(K k, V v) {
+        if (!containsKey(k)) {
+            return put(k, v);
+        } else {
+            return get(k);
+        }
+    }
+
+    @Override
+    public synchronized boolean remove(Object k, Object v) {
+        if (containsKey(k) && get(k).equals(v)) {
+            remove(k);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public synchronized boolean replace(K k, V original, V replacement) {
+        if (containsKey(k) && get(k).equals(original)) {
+            put(k, replacement);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public synchronized V replace(K k, V v) {
+        if (containsKey(k)) {
+            return put(k, v);
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/util/IgnoreHashEquals.java
similarity index 64%
copy from src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
copy to src/main/java/org/apache/skywalking/banyandb/v1/client/util/IgnoreHashEquals.java
index 9732233..fd680c1 100644
--- a/src/main/java/org/apache/skywalking/banyandb/v1/client/metadata/Catalog.java
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/util/IgnoreHashEquals.java
@@ -16,17 +16,17 @@
  *
  */
 
-package org.apache.skywalking.banyandb.v1.client.metadata;
+package org.apache.skywalking.banyandb.v1.client.util;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.skywalking.banyandb.v1.Banyandb;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
 
-@RequiredArgsConstructor
-public enum Catalog {
-    STREAM(Banyandb.Catalog.CATALOG_STREAM), MEASURE(Banyandb.Catalog.CATALOG_MEASURE);
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-    @Getter(AccessLevel.PACKAGE)
-    private final Banyandb.Catalog catalog;
+@Retention(SOURCE)
+@Target({METHOD, PARAMETER, FIELD})
+public @interface IgnoreHashEquals {
 }
diff --git a/src/main/java/org/apache/skywalking/banyandb/v1/client/util/PrivateKeyUtil.java b/src/main/java/org/apache/skywalking/banyandb/v1/client/util/PrivateKeyUtil.java
new file mode 100644
index 0000000..c77fb0b
--- /dev/null
+++ b/src/main/java/org/apache/skywalking/banyandb/v1/client/util/PrivateKeyUtil.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.banyandb.v1.client.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Base64;
+
+/**
+ * Util intends to parse PKCS#1 and PKCS#8 at same time.
+ */
+public class PrivateKeyUtil {
+    private static final String PKCS_1_PEM_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
+    private static final String PKCS_1_PEM_FOOTER = "-----END RSA PRIVATE KEY-----";
+    private static final String PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
+    private static final String PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----";
+
+    /**
+     * Load a RSA decryption key from a file (PEM or DER).
+     */
+    public static InputStream loadDecryptionKey(String keyFilePath) throws IOException {
+        byte[] keyDataBytes = Files.readAllBytes(Paths.get(keyFilePath));
+        String keyDataString = new String(keyDataBytes, StandardCharsets.UTF_8);
+
+        if (keyDataString.contains(PKCS_1_PEM_HEADER)) {
+            // OpenSSL / PKCS#1 Base64 PEM encoded file
+            keyDataString = keyDataString.replace(PKCS_1_PEM_HEADER, "");
+            keyDataString = keyDataString.replace(PKCS_1_PEM_FOOTER, "");
+            keyDataString = keyDataString.replace("\n", "");
+            return readPkcs1PrivateKey(Base64.getDecoder().decode(keyDataString));
+        }
+
+        return new ByteArrayInputStream(keyDataString.getBytes());
+    }
+
+    /**
+     * Create a InputStream instance from raw PKCS#1 bytes. Raw Java API can't recognize ASN.1 format, so we should
+     * convert it into a pkcs#8 format Java can understand.
+     */
+    private static InputStream readPkcs1PrivateKey(byte[] pkcs1Bytes) {
+        int pkcs1Length = pkcs1Bytes.length;
+        int totalLength = pkcs1Length + 22;
+        byte[] pkcs8Header = new byte[]{
+                0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff), (byte) (totalLength & 0xff), // Sequence + total length
+                0x2, 0x1, 0x0, // Integer (0)
+                0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0, // Sequence: 1.2.840.113549.1.1.1, NULL
+                0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) (pkcs1Length & 0xff) // Octet string + length
+        };
+        StringBuilder pkcs8 = new StringBuilder(PKCS_8_PEM_HEADER);
+        pkcs8.append("\n").append(new String(Base64.getEncoder().encode(join(pkcs8Header, pkcs1Bytes))));
+        pkcs8.append("\n").append(PKCS_8_PEM_FOOTER);
+        return new ByteArrayInputStream(pkcs8.toString().getBytes());
+    }
+
+    private static byte[] join(byte[] byteArray1, byte[] byteArray2) {
+        byte[] bytes = new byte[byteArray1.length + byteArray2.length];
+        System.arraycopy(byteArray1, 0, bytes, 0, byteArray1.length);
+        System.arraycopy(byteArray2, 0, bytes, byteArray1.length, byteArray2.length);
+        return bytes;
+    }
+}
diff --git a/src/main/proto/banyandb/v1/banyandb-common.proto b/src/main/proto/banyandb/v1/banyandb-common.proto
new file mode 100644
index 0000000..c5f0e26
--- /dev/null
+++ b/src/main/proto/banyandb/v1/banyandb-common.proto
@@ -0,0 +1,64 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.common.v1";
+
+package banyandb.common.v1;
+
+import "google/protobuf/timestamp.proto";
+
+enum Catalog {
+  CATALOG_UNSPECIFIED = 0;
+  CATALOG_STREAM = 1;
+  CATALOG_MEASURE = 2;
+}
+
+// Metadata is for multi-tenant, multi-model use
+message Metadata {
+  // group contains a set of options, like retention policy, max
+  string group = 1;
+  // name of the entity
+  string name = 2;
+  uint32 id = 3;
+  // readonly. create_revision is the revision of last creation on this key.
+  int64 create_revision = 4;
+  // readonly. mod_revision is the revision of last modification on this key.
+  int64 mod_revision = 5;
+}
+
+message ResourceOpts {
+  // shard_num is the number of shards
+  uint32 shard_num = 1;
+  // block_num specific how many blocks in a segment
+  uint32 block_num = 2;
+  // ttl indicates time to live, how long the data will be cached
+  string ttl = 3;
+}
+
+// Group is an internal object for Group management
+message Group {
+  // metadata define the group's identity
+  common.v1.Metadata metadata = 1;
+  // catalog denotes which type of data the group contains
+  common.v1.Catalog catalog = 2;
+  // resourceOpts indicates the structure of the underlying kv storage
+  ResourceOpts resource_opts = 3;
+  // updated_at indicates when resources of the group are updated
+  google.protobuf.Timestamp updated_at = 4;
+}
diff --git a/src/main/proto/banyandb/v1/banyandb-metadata.proto b/src/main/proto/banyandb/v1/banyandb-database.proto
similarity index 68%
rename from src/main/proto/banyandb/v1/banyandb-metadata.proto
rename to src/main/proto/banyandb/v1/banyandb-database.proto
index 1e76318..7115aff 100644
--- a/src/main/proto/banyandb/v1/banyandb-metadata.proto
+++ b/src/main/proto/banyandb/v1/banyandb-database.proto
@@ -17,40 +17,22 @@
 
 syntax = "proto3";
 
-option java_package = "org.apache.skywalking.banyandb.database.v1.metadata";
+option java_package = "org.apache.skywalking.banyandb.database.v1";
 
-package banyandb.metadata.v1;
+package banyandb.database.v1;
 
 import "google/protobuf/timestamp.proto";
-import "banyandb/v1/banyandb.proto";
-
-message ResourceOpts {
-  // shard_num is the number of shards
-  uint32 shard_num = 1;
-  // ttl indicates time to live, how long the data will be cached
-  Duration ttl = 2;
-}
-
-// Duration represents the elapsed time between two instants
-message Duration {
-  uint32 val = 1;
-  enum DurationUnit {
-    DURATION_UNIT_UNSPECIFIED = 0;
-    DURATION_UNIT_HOUR = 1;
-    DURATION_UNIT_DAY = 2;
-    DURATION_UNIT_WEEK = 3;
-    DURATION_UNIT_MONTH = 4;
-  }
-  DurationUnit unit = 2;
-}
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
 
 enum TagType {
-  TAG_TYPE_UNSPECIFIED = 0;
+  TAG_TYPE_UNSPECIFIED=0;
   TAG_TYPE_STRING = 1;
   TAG_TYPE_INT = 2;
   TAG_TYPE_STRING_ARRAY = 3;
   TAG_TYPE_INT_ARRAY = 4;
   TAG_TYPE_DATA_BINARY = 5;
+  TAG_TYPE_ID = 6;
 }
 
 message TagFamilySpec {
@@ -67,14 +49,13 @@ message TagSpec {
 // Stream intends to store streaming data, for example, traces or logs
 message Stream {
   // metadata is the identity of a trace series
-  banyandb.v1.Metadata metadata = 1;
+  common.v1.Metadata metadata = 1;
   // tag_families
   repeated TagFamilySpec tag_families = 2;
   // entity indicates how to generate a series and shard a stream
   Entity entity = 3;
-  ResourceOpts opts = 4;
-  // updated_at_nanoseconds indicates when the TraceSeries is updated
-  google.protobuf.Timestamp updated_at_nanoseconds = 5;
+  // updated_at indicates when the stream is updated
+  google.protobuf.Timestamp updated_at = 4;
 }
 
 message Entity {
@@ -110,42 +91,50 @@ message FieldSpec {
   CompressionMethod compression_method = 4;
 }
 
-// IntervalRule indicates how to get the interval of a measure series.
-message IntervalRule {
-  // tag_name is for matching a tag
-  string tag_name = 1;
-  // tag_value is used by a equality matcher to match a tag
-  oneof tag_value {
-    string str = 2;
-    int64 int = 3;
-  }
-  // interval indicates how frequently to send a data point
-  string interval = 4;
-}
-
 // Measure intends to store data point
 message Measure {
   // metadata is the identity of a measure
-  banyandb.v1.Metadata metadata = 1;
+  common.v1.Metadata metadata = 1;
   // tag_families are for filter measures
   repeated TagFamilySpec tag_families = 2;
   // fields denote measure values
   repeated FieldSpec fields = 3;
   // entity indicates which tags will be to generate a series and shard a measure
   Entity entity = 4;
-  // interval_rules define data points writing interval
-  repeated IntervalRule interval_rules = 5;
-  // opts is basic resource management options
-  ResourceOpts opts = 6;
-  // updated_at_nanoseconds indicates when the measure is updated
-  google.protobuf.Timestamp updated_at_nanoseconds = 7;
+  // interval indicates how frequently to send a data point
+  string interval = 5;
+  // updated_at indicates when the measure is updated
+  google.protobuf.Timestamp updated_at = 6;
+}
+
+// TopNAggregation generates offline TopN statistics for a measure's TopN approximation
+message TopNAggregation {
+  // metadata is the identity of an aggregation
+  common.v1.Metadata metadata = 1;
+  // source_measure denotes the data source of this aggregation
+  common.v1.Metadata source_measure = 2;
+  // field_name is the name of field used for ranking
+  string field_name = 3;
+  // field_value_sort indicates how to sort fields
+  // ASC: bottomN
+  // DESC: topN
+  // UNSPECIFIED: topN + bottomN
+  model.v1.Sort field_value_sort = 4;
+  // group_by_tag_names groups data points into statistical counters
+  repeated string group_by_tag_names = 5;
+  // criteria select partial data points from measure
+  repeated model.v1.Criteria criteria = 6;
+  // counters_number sets the number of counters to be tracked. The default value is 1000
+  int32 counters_number = 7;
+  // updated_at indicates when the measure is updated
+  google.protobuf.Timestamp updated_at = 8;
 }
 
 // IndexRule defines how to generate indices based on tags and the index type
 // IndexRule should bind to a subject through an IndexRuleBinding to generate proper indices.
 message IndexRule {
   // metadata define the rule's identity
-  banyandb.v1.Metadata metadata = 1;
+  common.v1.Metadata metadata = 1;
   // tags are the combination that refers to an indexed object
   // If the elements in tags are more than 1, the object will generate a multi-tag index
   // Caveat: All tags in a multi-tag MUST have an identical IndexType
@@ -165,24 +154,24 @@ message IndexRule {
   }
   // location indicates where to store index.
   Location location = 4;
-  // updated_at_nanoseconds indicates when the IndexRule is updated
+  // updated_at indicates when the IndexRule is updated
   google.protobuf.Timestamp updated_at = 5;
 }
 
 // Subject defines which stream or measure would generate indices
 message Subject {
   // catalog is where the subject belongs to
-  banyandb.v1.Catalog catalog = 1;
+  common.v1.Catalog catalog = 1;
   // name refers to a stream or measure in a particular catalog
   string name = 2;
 }
 
-// IndexRuleBinding is a bridge to connect several IndexRules to a subject
+// IndexRuleBinding is a bridge to connect severalIndexRules to a subject
 // This binding is valid between begin_at_nanoseconds and expire_at_nanoseconds, that provides flexible strategies
 // to control how to generate time series indices.
 message IndexRuleBinding {
   // metadata is the identity of this binding
-  banyandb.v1.Metadata metadata = 1;
+  common.v1.Metadata metadata = 1;
   // rules refers to the IndexRule
   repeated string rules = 2;
   // subject indicates the subject of binding action
@@ -192,26 +181,26 @@ message IndexRuleBinding {
   // expire_at_nanoseconds it the timestamp, after which the binding will be inactive
   // expire_at_nanoseconds must be larger than begin_at_nanoseconds
   google.protobuf.Timestamp expire_at = 5;
-  // updated_at_nanoseconds indicates when the IndexRuleBinding is updated
+  // updated_at indicates when the IndexRuleBinding is updated
   google.protobuf.Timestamp updated_at = 6;
 }
 
 message StreamRegistryServiceCreateRequest {
-  banyandb.metadata.v1.Stream stream = 1;
+  banyandb.database.v1.Stream stream = 1;
 }
 
 message StreamRegistryServiceCreateResponse {
 }
 
 message StreamRegistryServiceUpdateRequest {
-  banyandb.metadata.v1.Stream stream = 1;
+  banyandb.database.v1.Stream stream = 1;
 }
 
 message StreamRegistryServiceUpdateResponse {
 }
 
 message StreamRegistryServiceDeleteRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message StreamRegistryServiceDeleteResponse {
@@ -219,11 +208,11 @@ message StreamRegistryServiceDeleteResponse {
 }
 
 message StreamRegistryServiceGetRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message StreamRegistryServiceGetResponse {
-  banyandb.metadata.v1.Stream stream = 1;
+  banyandb.database.v1.Stream stream = 1;
 }
 
 message StreamRegistryServiceListRequest {
@@ -231,7 +220,7 @@ message StreamRegistryServiceListRequest {
 }
 
 message StreamRegistryServiceListResponse {
-  repeated banyandb.metadata.v1.Stream stream = 1;
+  repeated banyandb.database.v1.Stream stream = 1;
 }
 
 service StreamRegistryService {
@@ -243,21 +232,21 @@ service StreamRegistryService {
 }
 
 message IndexRuleBindingRegistryServiceCreateRequest {
-  banyandb.metadata.v1.IndexRuleBinding index_rule_binding = 1;
+  banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
 }
 
 message IndexRuleBindingRegistryServiceCreateResponse {
 }
 
 message IndexRuleBindingRegistryServiceUpdateRequest {
-  banyandb.metadata.v1.IndexRuleBinding index_rule_binding = 1;
+  banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
 }
 
 message IndexRuleBindingRegistryServiceUpdateResponse {
 }
 
 message IndexRuleBindingRegistryServiceDeleteRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message IndexRuleBindingRegistryServiceDeleteResponse {
@@ -265,11 +254,11 @@ message IndexRuleBindingRegistryServiceDeleteResponse {
 }
 
 message IndexRuleBindingRegistryServiceGetRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message IndexRuleBindingRegistryServiceGetResponse {
-  banyandb.metadata.v1.IndexRuleBinding index_rule_binding = 1;
+  banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
 }
 
 message IndexRuleBindingRegistryServiceListRequest {
@@ -277,7 +266,7 @@ message IndexRuleBindingRegistryServiceListRequest {
 }
 
 message IndexRuleBindingRegistryServiceListResponse {
-  repeated banyandb.metadata.v1.IndexRuleBinding index_rule_binding = 1;
+  repeated banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
 }
 
 service IndexRuleBindingRegistryService {
@@ -289,21 +278,21 @@ service IndexRuleBindingRegistryService {
 }
 
 message IndexRuleRegistryServiceCreateRequest {
-  banyandb.metadata.v1.IndexRule index_rule = 1;
+  banyandb.database.v1.IndexRule index_rule = 1;
 }
 
 message IndexRuleRegistryServiceCreateResponse {
 }
 
 message IndexRuleRegistryServiceUpdateRequest {
-  banyandb.metadata.v1.IndexRule index_rule = 1;
+  banyandb.database.v1.IndexRule index_rule = 1;
 }
 
 message IndexRuleRegistryServiceUpdateResponse {
 }
 
 message IndexRuleRegistryServiceDeleteRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message IndexRuleRegistryServiceDeleteResponse {
@@ -311,11 +300,11 @@ message IndexRuleRegistryServiceDeleteResponse {
 }
 
 message IndexRuleRegistryServiceGetRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message IndexRuleRegistryServiceGetResponse {
-  banyandb.metadata.v1.IndexRule index_rule = 1;
+  banyandb.database.v1.IndexRule index_rule = 1;
 }
 
 message IndexRuleRegistryServiceListRequest {
@@ -323,7 +312,7 @@ message IndexRuleRegistryServiceListRequest {
 }
 
 message IndexRuleRegistryServiceListResponse {
-  repeated banyandb.metadata.v1.IndexRule index_rule = 1;
+  repeated banyandb.database.v1.IndexRule index_rule = 1;
 }
 
 service IndexRuleRegistryService {
@@ -335,21 +324,21 @@ service IndexRuleRegistryService {
 }
 
 message MeasureRegistryServiceCreateRequest {
-  banyandb.metadata.v1.Measure measure = 1;
+  banyandb.database.v1.Measure measure = 1;
 }
 
 message MeasureRegistryServiceCreateResponse {
 }
 
 message MeasureRegistryServiceUpdateRequest {
-  banyandb.metadata.v1.Measure measure = 1;
+  banyandb.database.v1.Measure measure = 1;
 }
 
 message MeasureRegistryServiceUpdateResponse {
 }
 
 message MeasureRegistryServiceDeleteRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message MeasureRegistryServiceDeleteResponse {
@@ -357,11 +346,11 @@ message MeasureRegistryServiceDeleteResponse {
 }
 
 message MeasureRegistryServiceGetRequest {
-  banyandb.v1.Metadata metadata = 1;
+  banyandb.common.v1.Metadata metadata = 1;
 }
 
 message MeasureRegistryServiceGetResponse {
-  banyandb.metadata.v1.Measure measure = 1;
+  banyandb.database.v1.Measure measure = 1;
 }
 
 message MeasureRegistryServiceListRequest {
@@ -369,7 +358,7 @@ message MeasureRegistryServiceListRequest {
 }
 
 message MeasureRegistryServiceListResponse {
-  repeated banyandb.metadata.v1.Measure measure = 1;
+  repeated banyandb.database.v1.Measure measure = 1;
 }
 
 service MeasureRegistryService {
@@ -379,3 +368,50 @@ service MeasureRegistryService {
   rpc Get(MeasureRegistryServiceGetRequest) returns (MeasureRegistryServiceGetResponse);
   rpc List(MeasureRegistryServiceListRequest) returns (MeasureRegistryServiceListResponse);
 }
+
+message GroupRegistryServiceCreateRequest {
+  banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceCreateResponse {
+
+}
+
+message GroupRegistryServiceUpdateRequest {
+  banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceUpdateResponse {
+
+}
+
+message GroupRegistryServiceDeleteRequest {
+  string group = 1;
+}
+
+message GroupRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message GroupRegistryServiceGetRequest {
+  string group = 1;
+}
+
+message GroupRegistryServiceGetResponse {
+  banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceListRequest {
+}
+
+message GroupRegistryServiceListResponse {
+  repeated banyandb.common.v1.Group group = 1;
+}
+
+service GroupRegistryService {
+  rpc Create(GroupRegistryServiceCreateRequest) returns (GroupRegistryServiceCreateResponse);
+  rpc Update(GroupRegistryServiceUpdateRequest) returns (GroupRegistryServiceUpdateResponse);
+  rpc Delete(GroupRegistryServiceDeleteRequest) returns (GroupRegistryServiceDeleteResponse);
+  rpc Get(GroupRegistryServiceGetRequest) returns (GroupRegistryServiceGetResponse);
+  rpc List(GroupRegistryServiceListRequest) returns (GroupRegistryServiceListResponse);
+}
diff --git a/src/main/proto/banyandb/v1/banyandb-measure.proto b/src/main/proto/banyandb/v1/banyandb-measure.proto
new file mode 100644
index 0000000..28d90d9
--- /dev/null
+++ b/src/main/proto/banyandb/v1/banyandb-measure.proto
@@ -0,0 +1,146 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.measure.v1";
+
+package banyandb.measure.v1;
+
+import "google/protobuf/timestamp.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
+
+// DataPoint is stored in Measures
+message DataPoint {
+  // timestamp is in the timeunit of nanoseconds.
+  google.protobuf.Timestamp timestamp = 1;
+  // tag_families contains tags selected in the projection
+  repeated model.v1.TagFamily tag_families = 2;
+  message Field {
+    string name = 1;
+    model.v1.FieldValue value = 2;
+  }
+  // fields contains fields selected in the projection
+  repeated Field fields = 3;
+}
+
+// QueryResponse is the response for a query to the Query module.
+message QueryResponse {
+  // data_points are the actual data returned
+  repeated DataPoint data_points = 1;
+}
+
+// QueryRequest is the request contract for query.
+message QueryRequest {
+  // metadata is required
+  common.v1.Metadata metadata = 1;
+  // time_range is a range query with begin/end time of entities in the timeunit of nanoseconds.
+  model.v1.TimeRange time_range = 2;
+  // tag_families are indexed.
+  repeated model.v1.Criteria criteria = 4;
+  // tag_projection can be used to select tags of the data points in the response
+  model.v1.TagProjection tag_projection = 5;
+  message FieldProjection {
+    repeated string names = 1;
+  }
+  // field_projection can be used to select fields of the data points in the response
+  FieldProjection field_projection = 6;
+  message GroupBy {
+    // tag_projection must be a subset of the tag_projection of QueryRequest
+    model.v1.TagProjection tag_projection = 1;
+    // field_name must be one of fields indicated by field_projection
+    string field_name = 2;
+  }
+  // group_by groups data points based on their field value for a specific tag and use field_name as the projection name
+  GroupBy group_by = 7;
+  message Aggregation {
+    model.v1.AggregationFunction function = 1;
+    // field_name must be one of files indicated by the field_projection
+    string field_name = 2;
+  }
+  // agg aggregates data points based on a field
+  Aggregation agg = 8;
+}
+
+//TopNList contains a series of topN items
+message TopNList {
+  // timestamp is in the timeunit of nanoseconds.
+  google.protobuf.Timestamp timestamp = 1;
+  message Item {
+    string name = 1;
+    model.v1.FieldValue value = 2;
+  }
+  // items contains top-n items in a list
+  repeated Item items = 2;
+}
+
+// TopNResponse is the response for a query to the Query module.
+message TopNResponse {
+  // lists contain a series topN lists ranked by timestamp
+  // if agg_func in query request is specified, lists' size should be one.
+  repeated TopNList lists = 1;
+}
+
+// TopNRequest is the request contract for query.
+message TopNRequest {
+  // metadata is required
+  common.v1.Metadata metadata = 1;
+  // time_range is a range query with begin/end time of entities in the timeunit of nanoseconds.
+  model.v1.TimeRange time_range = 2;
+  // top_n set the how many items should be returned in each list.
+  int32 top_n = 3;
+  // agg aggregates lists grouped by field names in the time_range
+  model.v1.AggregationFunction agg = 4;
+  // criteria select counters.
+  repeated model.v1.Condition conditions = 5;
+  // field_value_sort indicates how to sort fields
+  model.v1.Sort field_value_sort = 6;
+}
+
+// DataPointValue is the data point for writing. It only contains values.
+message DataPointValue {
+  // timestamp is in the timeunit of nanoseconds.
+  google.protobuf.Timestamp timestamp = 1;
+  // the order of tag_families' items match the measure schema
+  repeated model.v1.TagFamilyForWrite tag_families = 2;
+  // the order of fields match the measure schema
+  repeated model.v1.FieldValue fields = 3;
+}
+
+// WriteRequest is the request contract for write
+message WriteRequest {
+  // the metadata is required.
+  common.v1.Metadata metadata = 1;
+  // the data_point is required.
+  DataPointValue data_point = 2;
+}
+
+// WriteResponse is the response contract for write
+message WriteResponse {}
+
+message InternalWriteRequest {
+  uint32 shard_id = 1;
+  bytes series_hash = 2;
+  WriteRequest request = 3;
+}
+
+service MeasureService {
+  rpc Query(banyandb.measure.v1.QueryRequest) returns (banyandb.measure.v1.QueryResponse);
+  rpc Write(stream banyandb.measure.v1.WriteRequest) returns (stream banyandb.measure.v1.WriteResponse);
+  rpc TopN(banyandb.measure.v1.TopNRequest) returns (banyandb.measure.v1.TopNResponse);
+}
\ No newline at end of file
diff --git a/src/main/proto/banyandb/v1/banyandb.proto b/src/main/proto/banyandb/v1/banyandb-model.proto
similarity index 79%
rename from src/main/proto/banyandb/v1/banyandb.proto
rename to src/main/proto/banyandb/v1/banyandb-model.proto
index 58f1064..23b6eae 100644
--- a/src/main/proto/banyandb/v1/banyandb.proto
+++ b/src/main/proto/banyandb/v1/banyandb-model.proto
@@ -17,26 +17,15 @@
 
 syntax = "proto3";
 
-option java_package = "org.apache.skywalking.banyandb.v1";
+option java_package = "org.apache.skywalking.banyandb.model.v1";
 
-package banyandb.v1;
+package banyandb.model.v1;
 
 import "google/protobuf/timestamp.proto";
 import "google/protobuf/struct.proto";
 
-enum Catalog {
-  CATALOG_UNSPECIFIED = 0;
-  CATALOG_STREAM = 1;
-  CATALOG_MEASURE = 2;
-}
-
-// Metadata is for multi-tenant, multi-model use
-message Metadata {
-  // group contains a set of options, like retention policy, max
-  string group = 1;
-  // name of the entity
-  string name = 2;
-  uint32 id = 3;
+message ID {
+  string value = 1;
 }
 
 message Str {
@@ -63,6 +52,7 @@ message TagValue {
     Int int = 4;
     IntArray int_array = 5;
     bytes binary_data = 6;
+    ID id = 7;
   }
 }
 
@@ -70,6 +60,24 @@ message TagFamilyForWrite {
   repeated TagValue tags = 1;
 }
 
+message FieldValue {
+  oneof value {
+    google.protobuf.NullValue null = 1;
+    model.v1.Str str = 2;
+    model.v1.Int int = 3;
+    bytes binary_data = 4;
+  }
+}
+
+enum AggregationFunction {
+  AGGREGATION_FUNCTION_UNSPECIFIED = 0;
+  AGGREGATION_FUNCTION_MEAN = 1;
+  AGGREGATION_FUNCTION_MAX = 2;
+  AGGREGATION_FUNCTION_MIN = 3;
+  AGGREGATION_FUNCTION_COUNT = 4;
+  AGGREGATION_FUNCTION_SUM = 5;
+}
+
 // Pair is the building block of a record which is equivalent to a key-value pair.
 // In the context of Trace, it could be metadata of a trace such as service_name, service_instance, etc.
 // Besides, other tags are organized in key-value pair in the underlying storage layer.
@@ -102,31 +110,39 @@ message Condition {
     BINARY_OP_GE = 6;
     BINARY_OP_HAVING = 7;
     BINARY_OP_NOT_HAVING = 8;
+    BINARY_OP_IN = 9;
+    BINARY_OP_NOT_IN = 10;
   }
   string name = 1;
   BinaryOp op = 2;
   TagValue value = 3;
 }
 
+// tag_families are indexed.
+message Criteria {
+  string tag_family_name = 1;
+  repeated model.v1.Condition conditions = 2;
+}
+
+enum Sort {
+  SORT_UNSPECIFIED = 0;
+  SORT_DESC = 1;
+  SORT_ASC = 2;
+}
+
 // QueryOrder means a Sort operation to be done for a given index rule.
 // The index_rule_name refers to the name of a index rule bound to the subject.
 message QueryOrder {
   string index_rule_name = 1;
-  enum Sort {
-    SORT_UNSPECIFIED = 0;
-    SORT_DESC = 1;
-    SORT_ASC = 2;
-  }
   Sort sort = 2;
 }
 
-// Projection is used to select the names of keys to be returned.
-message Projection {
+// TagProjection is used to select the names of keys to be returned.
+message TagProjection {
   message TagFamily {
     string name = 1;
     repeated string tags = 2;
   }
-  // The key_name refers to the key(s) of Pair(s).
   repeated TagFamily tag_families = 1;
 }
 
@@ -136,3 +152,4 @@ message TimeRange {
   google.protobuf.Timestamp begin = 1;
   google.protobuf.Timestamp end = 2;
 }
+
diff --git a/src/main/proto/banyandb/v1/banyandb-stream.proto b/src/main/proto/banyandb/v1/banyandb-stream.proto
index 52b76a7..6abb63d 100644
--- a/src/main/proto/banyandb/v1/banyandb-stream.proto
+++ b/src/main/proto/banyandb/v1/banyandb-stream.proto
@@ -17,12 +17,13 @@
 
 syntax = "proto3";
 
-option java_package = "org.apache.skywalking.banyandb.v1.stream";
+option java_package = "org.apache.skywalking.banyandb.stream.v1";
 
 package banyandb.stream.v1;
 
 import "google/protobuf/timestamp.proto";
-import "banyandb/v1/banyandb.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
 
 // Element represents
 // (stream context) a Span defined in Google Dapper paper or equivalently a Segment in Skywalking.
@@ -40,7 +41,7 @@ message Element {
   // - service_name
   // - service_instance_id
   // - end_time_nanoseconds
-  repeated banyandb.v1.TagFamily tag_families = 3;
+  repeated model.v1.TagFamily tag_families = 3;
 }
 
 // QueryResponse is the response for a query to the Query module.
@@ -52,26 +53,22 @@ message QueryResponse {
 // QueryRequest is the request contract for query.
 message QueryRequest {
   // metadata is required
-  banyandb.v1.Metadata metadata = 1;
+  common.v1.Metadata metadata = 1;
   // time_range is a range query with begin/end time of entities in the timeunit of nanoseconds.
   // In the context of stream, it represents the range of the `startTime` for spans/segments,
   // while in the context of Log, it means the range of the timestamp(s) for logs.
   // it is always recommended to specify time range for performance reason
-  banyandb.v1.TimeRange time_range = 2;
+  model.v1.TimeRange time_range = 2;
   // offset is used to support pagination, together with the following limit
   uint32 offset = 3;
   // limit is used to impose a boundary on the number of records being returned
   uint32 limit = 4;
   // order_by is given to specify the sort for a field. So far, only fields in the type of Integer are supported
-  banyandb.v1.QueryOrder order_by = 5;
+  model.v1.QueryOrder order_by = 5;
   // tag_families are indexed.
-  message Criteria {
-    string tag_family_name = 1;
-    repeated banyandb.v1.Condition conditions = 2;
-  }
-  repeated Criteria criteria = 6;
+  repeated model.v1.Criteria criteria = 6;
   // projection can be used to select the key names of the element in the response
-  banyandb.v1.Projection projection = 7;
+  model.v1.TagProjection projection = 7;
 }
 
 message ElementValue {
@@ -82,12 +79,12 @@ message ElementValue {
   // 2) or the timestamp of a log
   google.protobuf.Timestamp timestamp = 2;
   // the order of tag_families' items match the stream schema
-  repeated banyandb.v1.TagFamilyForWrite tag_families = 3;
+  repeated model.v1.TagFamilyForWrite tag_families = 3;
 }
 
 message WriteRequest {
   // the metadata is only required in the first write.
-  banyandb.v1.Metadata metadata = 1;
+  common.v1.Metadata metadata = 1;
   // the element is required.
   ElementValue element = 2;
 }
@@ -97,4 +94,4 @@ message WriteResponse {}
 service StreamService {
   rpc Query(banyandb.stream.v1.QueryRequest) returns (banyandb.stream.v1.QueryResponse);
   rpc Write(stream banyandb.stream.v1.WriteRequest) returns (stream banyandb.stream.v1.WriteResponse);
-}
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/AbstractBanyanDBClientTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/AbstractBanyanDBClientTest.java
new file mode 100644
index 0000000..3c19157
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/AbstractBanyanDBClientTest.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import io.grpc.BindableService;
+import io.grpc.ManagedChannel;
+import io.grpc.Server;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.internal.AbstractServerImplBuilder;
+import io.grpc.stub.StreamObserver;
+import io.grpc.testing.GrpcCleanupRule;
+import io.grpc.util.MutableHandlerRegistry;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.IndexRuleBindingRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.database.v1.IndexRuleRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.database.v1.MeasureRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.database.v1.StreamRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.util.TimeUtils;
+import org.junit.Rule;
+
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.powermock.api.mockito.PowerMockito.mock;
+
+public class AbstractBanyanDBClientTest {
+    @Rule
+    public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
+
+    // play as an in-memory registry
+    protected Map<String, BanyandbDatabase.IndexRuleBinding> indexRuleBindingRegistry;
+
+    private final IndexRuleBindingRegistryServiceGrpc.IndexRuleBindingRegistryServiceImplBase indexRuleBindingServiceImpl =
+            mock(IndexRuleBindingRegistryServiceGrpc.IndexRuleBindingRegistryServiceImplBase.class, delegatesTo(
+                    new IndexRuleBindingRegistryServiceGrpc.IndexRuleBindingRegistryServiceImplBase() {
+                        @Override
+                        public void create(BanyandbDatabase.IndexRuleBindingRegistryServiceCreateRequest request, StreamObserver<BanyandbDatabase.IndexRuleBindingRegistryServiceCreateResponse> responseObserver) {
+                            BanyandbDatabase.IndexRuleBinding s = request.getIndexRuleBinding().toBuilder()
+                                    .setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            indexRuleBindingRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleBindingRegistryServiceCreateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void update(BanyandbDatabase.IndexRuleBindingRegistryServiceUpdateRequest request, StreamObserver<BanyandbDatabase.IndexRuleBindingRegistryServiceUpdateResponse> responseObserver) {
+                            BanyandbDatabase.IndexRuleBinding s = request.getIndexRuleBinding().toBuilder()
+                                    .setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            indexRuleBindingRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleBindingRegistryServiceUpdateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void delete(BanyandbDatabase.IndexRuleBindingRegistryServiceDeleteRequest request, StreamObserver<BanyandbDatabase.IndexRuleBindingRegistryServiceDeleteResponse> responseObserver) {
+                            BanyandbDatabase.IndexRuleBinding oldIndexRuleBinding = indexRuleBindingRegistry.remove(request.getMetadata().getName());
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleBindingRegistryServiceDeleteResponse.newBuilder()
+                                    .setDeleted(oldIndexRuleBinding != null)
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void get(BanyandbDatabase.IndexRuleBindingRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.IndexRuleBindingRegistryServiceGetResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleBindingRegistryServiceGetResponse.newBuilder()
+                                    .setIndexRuleBinding(indexRuleBindingRegistry.get(request.getMetadata().getName()))
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void list(BanyandbDatabase.IndexRuleBindingRegistryServiceListRequest request, StreamObserver<BanyandbDatabase.IndexRuleBindingRegistryServiceListResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleBindingRegistryServiceListResponse.newBuilder()
+                                    .addAllIndexRuleBinding(indexRuleBindingRegistry.values())
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+                    }));
+
+    // play as an in-memory registry
+    protected Map<String, BanyandbDatabase.IndexRule> indexRuleRegistry;
+
+    private final IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase indexRuleServiceImpl =
+            mock(IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase.class, delegatesTo(
+                    new IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase() {
+                        @Override
+                        public void create(BanyandbDatabase.IndexRuleRegistryServiceCreateRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceCreateResponse> responseObserver) {
+                            BanyandbDatabase.IndexRule s = request.getIndexRule().toBuilder().setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            indexRuleRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleRegistryServiceCreateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void update(BanyandbDatabase.IndexRuleRegistryServiceUpdateRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceUpdateResponse> responseObserver) {
+                            BanyandbDatabase.IndexRule s = request.getIndexRule().toBuilder().setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            indexRuleRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleRegistryServiceUpdateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void delete(BanyandbDatabase.IndexRuleRegistryServiceDeleteRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceDeleteResponse> responseObserver) {
+                            BanyandbDatabase.IndexRule oldIndexRule = indexRuleRegistry.remove(request.getMetadata().getName());
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleRegistryServiceDeleteResponse.newBuilder()
+                                    .setDeleted(oldIndexRule != null)
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void get(BanyandbDatabase.IndexRuleRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceGetResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleRegistryServiceGetResponse.newBuilder()
+                                    .setIndexRule(indexRuleRegistry.get(request.getMetadata().getName()))
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void list(BanyandbDatabase.IndexRuleRegistryServiceListRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceListResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.IndexRuleRegistryServiceListResponse.newBuilder()
+                                    .addAllIndexRule(indexRuleRegistry.values())
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+                    }));
+
+    // stream registry
+    protected Map<String, BanyandbDatabase.Stream> streamRegistry;
+
+    private final StreamRegistryServiceGrpc.StreamRegistryServiceImplBase streamRegistryServiceImpl =
+            mock(StreamRegistryServiceGrpc.StreamRegistryServiceImplBase.class, delegatesTo(
+                    new StreamRegistryServiceGrpc.StreamRegistryServiceImplBase() {
+                        @Override
+                        public void create(BanyandbDatabase.StreamRegistryServiceCreateRequest request, StreamObserver<BanyandbDatabase.StreamRegistryServiceCreateResponse> responseObserver) {
+                            BanyandbDatabase.Stream s = request.getStream().toBuilder()
+                                    .setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            streamRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.StreamRegistryServiceCreateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void update(BanyandbDatabase.StreamRegistryServiceUpdateRequest request, StreamObserver<BanyandbDatabase.StreamRegistryServiceUpdateResponse> responseObserver) {
+                            BanyandbDatabase.Stream s = request.getStream().toBuilder()
+                                    .setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            streamRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.StreamRegistryServiceUpdateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void delete(BanyandbDatabase.StreamRegistryServiceDeleteRequest request, StreamObserver<BanyandbDatabase.StreamRegistryServiceDeleteResponse> responseObserver) {
+                            BanyandbDatabase.Stream oldStream = streamRegistry.remove(request.getMetadata().getName());
+                            responseObserver.onNext(BanyandbDatabase.StreamRegistryServiceDeleteResponse.newBuilder()
+                                    .setDeleted(oldStream != null)
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void get(BanyandbDatabase.StreamRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.StreamRegistryServiceGetResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.StreamRegistryServiceGetResponse.newBuilder()
+                                    .setStream(streamRegistry.get(request.getMetadata().getName()))
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void list(BanyandbDatabase.StreamRegistryServiceListRequest request, StreamObserver<BanyandbDatabase.StreamRegistryServiceListResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.StreamRegistryServiceListResponse.newBuilder()
+                                    .addAllStream(streamRegistry.values())
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+                    }));
+
+    // measure registry
+    protected Map<String, BanyandbDatabase.Measure> measureRegistry;
+
+    private final MeasureRegistryServiceGrpc.MeasureRegistryServiceImplBase measureRegistryServiceImpl =
+            mock(MeasureRegistryServiceGrpc.MeasureRegistryServiceImplBase.class, delegatesTo(
+                    new MeasureRegistryServiceGrpc.MeasureRegistryServiceImplBase() {
+                        @Override
+                        public void create(BanyandbDatabase.MeasureRegistryServiceCreateRequest request, StreamObserver<BanyandbDatabase.MeasureRegistryServiceCreateResponse> responseObserver) {
+                            BanyandbDatabase.Measure s = request.getMeasure().toBuilder()
+                                    .setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            measureRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.MeasureRegistryServiceCreateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void update(BanyandbDatabase.MeasureRegistryServiceUpdateRequest request, StreamObserver<BanyandbDatabase.MeasureRegistryServiceUpdateResponse> responseObserver) {
+                            BanyandbDatabase.Measure s = request.getMeasure().toBuilder()
+                                    .setUpdatedAt(TimeUtils.buildTimestamp(ZonedDateTime.now()))
+                                    .build();
+                            measureRegistry.put(s.getMetadata().getName(), s);
+                            responseObserver.onNext(BanyandbDatabase.MeasureRegistryServiceUpdateResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void delete(BanyandbDatabase.MeasureRegistryServiceDeleteRequest request, StreamObserver<BanyandbDatabase.MeasureRegistryServiceDeleteResponse> responseObserver) {
+                            BanyandbDatabase.Measure oldMeasure = measureRegistry.remove(request.getMetadata().getName());
+                            responseObserver.onNext(BanyandbDatabase.MeasureRegistryServiceDeleteResponse.newBuilder()
+                                    .setDeleted(oldMeasure != null)
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void get(BanyandbDatabase.MeasureRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.MeasureRegistryServiceGetResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.MeasureRegistryServiceGetResponse.newBuilder()
+                                    .setMeasure(measureRegistry.get(request.getMetadata().getName()))
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+
+                        @Override
+                        public void list(BanyandbDatabase.MeasureRegistryServiceListRequest request, StreamObserver<BanyandbDatabase.MeasureRegistryServiceListResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.MeasureRegistryServiceListResponse.newBuilder()
+                                    .addAllMeasure(measureRegistry.values())
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+                    }));
+
+    protected final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry();
+
+    protected BanyanDBClient client;
+
+    protected ManagedChannel channel;
+
+    protected void setUp(SetupFunction... setUpFunctions) throws IOException {
+        indexRuleRegistry = new HashMap<>();
+        serviceRegistry.addService(indexRuleServiceImpl);
+
+        indexRuleBindingRegistry = new HashMap<>();
+        serviceRegistry.addService(indexRuleBindingServiceImpl);
+        indexRuleBindingRegistry = new HashMap<>();
+
+        // Generate a unique in-process server name.
+        String serverName = InProcessServerBuilder.generateName();
+
+        // Create a server, add service, start, and register for automatic graceful shutdown.
+        InProcessServerBuilder serverBuilder = InProcessServerBuilder
+                .forName(serverName).directExecutor()
+                .fallbackHandlerRegistry(serviceRegistry);
+        for (final SetupFunction func : setUpFunctions) {
+            func.apply(serverBuilder);
+        }
+        final Server s = serverBuilder.build();
+        grpcCleanup.register(s.start());
+
+        // Create a client channel and register for automatic graceful shutdown.
+        this.channel = grpcCleanup.register(
+                InProcessChannelBuilder.forName(serverName).directExecutor().build());
+
+        client = new BanyanDBClient("127.0.0.1", s.getPort());
+        client.connect(this.channel);
+    }
+
+    protected interface SetupFunction {
+        void apply(AbstractServerImplBuilder<?> builder);
+    }
+
+    protected SetupFunction bindStreamRegistry() {
+        return b -> {
+            AbstractBanyanDBClientTest.this.streamRegistry = new HashMap<>();
+            serviceRegistry.addService(streamRegistryServiceImpl);
+        };
+    }
+
+    protected SetupFunction bindService(final BindableService bindableService) {
+        return b -> serviceRegistry.addService(bindableService);
+    }
+
+    protected SetupFunction bindMeasureRegistry() {
+        return b -> {
+            AbstractBanyanDBClientTest.this.measureRegistry = new HashMap<>();
+            serviceRegistry.addService(measureRegistryServiceImpl);
+        };
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientMeasureQueryTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientMeasureQueryTest.java
new file mode 100644
index 0000000..d9f483f
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientMeasureQueryTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.protobuf.Timestamp;
+import io.grpc.stub.StreamObserver;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.measure.v1.MeasureServiceGrpc;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Duration;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
+import org.apache.skywalking.banyandb.v1.client.metadata.Measure;
+import org.apache.skywalking.banyandb.v1.client.metadata.TagFamilySpec;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.mock;
+
+@RunWith(PowerMockRunner.class)
+@PowerMockIgnore("javax.management.*")
+public class BanyanDBClientMeasureQueryTest extends AbstractBanyanDBClientTest {
+    private final MeasureServiceGrpc.MeasureServiceImplBase measureQueryService =
+            mock(MeasureServiceGrpc.MeasureServiceImplBase.class, delegatesTo(
+                    new MeasureServiceGrpc.MeasureServiceImplBase() {
+                        @Override
+                        public void query(BanyandbMeasure.QueryRequest request, StreamObserver<BanyandbMeasure.QueryResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbMeasure.QueryResponse.newBuilder().build());
+                            responseObserver.onCompleted();
+                        }
+                    }));
+
+    @Before
+    public void setUp() throws IOException, BanyanDBException {
+        setUp(bindService(measureQueryService), bindMeasureRegistry());
+
+        Measure m = Measure.create("sw_metric", "service_cpm_minute", Duration.ofHours(1))
+                .setEntityRelativeTags("entity_id")
+                .addTagFamily(TagFamilySpec.create("default")
+                        .addTagSpec(TagFamilySpec.TagSpec.newIDTag("id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("entity_id"))
+                        .build())
+                .addField(Measure.FieldSpec.newIntField("total").compressWithZSTD().encodeWithGorilla().build())
+                .addField(Measure.FieldSpec.newIntField("value").compressWithZSTD().encodeWithGorilla().build())
+                .addIndex(IndexRule.create("scope", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.SERIES))
+                .build();
+        client.define(m);
+    }
+
+    @Test
+    public void testQuery_tableScan() throws BanyanDBException {
+        ArgumentCaptor<BanyandbMeasure.QueryRequest> requestCaptor = ArgumentCaptor.forClass(BanyandbMeasure.QueryRequest.class);
+
+        Instant end = Instant.now();
+        Instant begin = end.minus(15, ChronoUnit.MINUTES);
+        MeasureQuery query = new MeasureQuery("sw_metric", "service_cpm_minute",
+                new TimestampRange(begin.toEpochMilli(), end.toEpochMilli()),
+                ImmutableSet.of("id", "entity_id"),
+                ImmutableSet.of("total"));
+        query.maxBy("total", ImmutableSet.of("entity_id"));
+        // search with conditions
+        query.appendCondition(PairQueryCondition.StringQueryCondition.eq("entity_id", "abc"));
+        client.query(query);
+
+        verify(measureQueryService).query(requestCaptor.capture(), ArgumentMatchers.any());
+
+        final BanyandbMeasure.QueryRequest request = requestCaptor.getValue();
+        // assert metadata
+        Assert.assertEquals("service_cpm_minute", request.getMetadata().getName());
+        Assert.assertEquals("sw_metric", request.getMetadata().getGroup());
+        // assert timeRange, both seconds and the nanos
+        Assert.assertEquals(begin.toEpochMilli() / 1000, request.getTimeRange().getBegin().getSeconds());
+        Assert.assertEquals(TimeUnit.MILLISECONDS.toNanos(begin.toEpochMilli() % 1000), request.getTimeRange().getBegin().getNanos());
+        Assert.assertEquals(end.toEpochMilli() / 1000, request.getTimeRange().getEnd().getSeconds());
+        Assert.assertEquals(TimeUnit.MILLISECONDS.toNanos(end.toEpochMilli() % 1000), request.getTimeRange().getEnd().getNanos());
+        // assert fields, we only have state as a condition which should be state
+        Assert.assertEquals(1, request.getCriteriaCount());
+        // assert state
+        Assert.assertEquals(BanyandbModel.Condition.BinaryOp.BINARY_OP_EQ, request.getCriteria(0).getConditions(0).getOp());
+        Assert.assertEquals(0L, request.getCriteria(0).getConditions(0).getValue().getInt().getValue());
+        // assert projections
+        assertCollectionEqual(Lists.newArrayList("default:id", "default:entity_id"),
+                parseProjectionList(request.getTagProjection()));
+        assertCollectionEqual(Lists.newArrayList("total"),
+                request.getFieldProjection().getNamesList());
+    }
+
+    @Test
+    public void testQuery_responseConversion() {
+        final String elementId = "1231.dfd.123123ssf";
+        final String entityIDValue = "entity_id_a";
+        final Instant now = Instant.now();
+        final BanyandbMeasure.QueryResponse responseObj = BanyandbMeasure.QueryResponse.newBuilder()
+                .addDataPoints(BanyandbMeasure.DataPoint.newBuilder()
+                        .setTimestamp(Timestamp.newBuilder()
+                                .setSeconds(now.toEpochMilli() / 1000)
+                                .setNanos((int) TimeUnit.MILLISECONDS.toNanos(now.toEpochMilli() % 1000))
+                                .build())
+                        .addTagFamilies(BanyandbModel.TagFamily.newBuilder()
+                                .setName("default")
+                                .addTags(BanyandbModel.Tag.newBuilder()
+                                        .setKey("id")
+                                        .setValue(BanyandbModel.TagValue.newBuilder()
+                                                .setId(BanyandbModel.ID.newBuilder().setValue(elementId).build()).build())
+                                        .build())
+                                .addTags(BanyandbModel.Tag.newBuilder()
+                                        .setKey("entity_id")
+                                        .setValue(BanyandbModel.TagValue.newBuilder()
+                                                .setStr(BanyandbModel.Str.newBuilder().setValue(entityIDValue).build()).build())
+                                        .build())
+                                .build())
+                        .addFields(BanyandbMeasure.DataPoint.Field.newBuilder()
+                                .setName("total")
+                                .setValue(BanyandbModel.FieldValue.newBuilder().setInt(
+                                        BanyandbModel.Int.newBuilder().setValue(10L).build()).build()
+                                ).build())
+                )
+                .build();
+        MeasureQueryResponse resp = new MeasureQueryResponse(responseObj);
+        Assert.assertNotNull(resp);
+        Assert.assertEquals(1, resp.getDataPoints().size());
+        Assert.assertEquals(2, resp.getDataPoints().get(0).getTags().size());
+        Assert.assertEquals(elementId,
+                resp.getDataPoints().get(0).getTagValue("id"));
+        Assert.assertEquals(entityIDValue, resp.getDataPoints().get(0).getTagValue("entity_id"));
+        Assert.assertEquals(10L,
+                (Number) resp.getDataPoints().get(0).getFieldValue("total"));
+    }
+
+    static <T> void assertCollectionEqual(Collection<T> c1, Collection<T> c2) {
+        Assert.assertTrue(c1.size() == c2.size() && c1.containsAll(c2) && c2.containsAll(c1));
+    }
+
+    static List<String> parseProjectionList(BanyandbModel.TagProjection projection) {
+        List<String> projectionList = new ArrayList<>();
+        for (int i = 0; i < projection.getTagFamiliesCount(); i++) {
+            final BanyandbModel.TagProjection.TagFamily tagFamily = projection.getTagFamilies(i);
+            for (int j = 0; j < tagFamily.getTagsCount(); j++) {
+                projectionList.add(tagFamily.getName() + ":" + tagFamily.getTags(j));
+            }
+        }
+        return projectionList;
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientMeasureWriteTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientMeasureWriteTest.java
new file mode 100644
index 0000000..1d7b02d
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientMeasureWriteTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import io.grpc.stub.StreamObserver;
+import org.apache.skywalking.banyandb.measure.v1.BanyandbMeasure;
+import org.apache.skywalking.banyandb.measure.v1.MeasureServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Duration;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
+import org.apache.skywalking.banyandb.v1.client.metadata.Measure;
+import org.apache.skywalking.banyandb.v1.client.metadata.TagFamilySpec;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class BanyanDBClientMeasureWriteTest extends AbstractBanyanDBClientTest {
+    private MeasureBulkWriteProcessor measureBulkWriteProcessor;
+
+    @Before
+    public void setUp() throws IOException, BanyanDBException {
+        measureRegistry = new HashMap<>();
+        setUp(bindMeasureRegistry());
+
+        measureBulkWriteProcessor = client.buildMeasureWriteProcessor(1000, 1, 1);
+
+        Measure m = Measure.create("sw_metric", "service_cpm_minute", Duration.ofHours(1))
+                .setEntityRelativeTags("entity_id")
+                .addTagFamily(TagFamilySpec.create("default")
+                        .addTagSpec(TagFamilySpec.TagSpec.newIDTag("id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("entity_id"))
+                        .build())
+                .addField(Measure.FieldSpec.newIntField("total").compressWithZSTD().encodeWithGorilla().build())
+                .addField(Measure.FieldSpec.newIntField("value").compressWithZSTD().encodeWithGorilla().build())
+                .addIndex(IndexRule.create("scope", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.SERIES))
+                .build();
+        client.define(m);
+    }
+
+    @After
+    public void shutdown() throws IOException {
+        measureBulkWriteProcessor.close();
+    }
+
+    @Test
+    public void testWrite() throws Exception {
+        final CountDownLatch allRequestsDelivered = new CountDownLatch(1);
+        final List<BanyandbMeasure.WriteRequest> writeRequestDelivered = new ArrayList<>();
+
+        // implement the fake service
+        final MeasureServiceGrpc.MeasureServiceImplBase serviceImpl =
+                new MeasureServiceGrpc.MeasureServiceImplBase() {
+                    @Override
+                    public StreamObserver<BanyandbMeasure.WriteRequest> write(StreamObserver<BanyandbMeasure.WriteResponse> responseObserver) {
+                        return new StreamObserver<BanyandbMeasure.WriteRequest>() {
+                            @Override
+                            public void onNext(BanyandbMeasure.WriteRequest value) {
+                                writeRequestDelivered.add(value);
+                                responseObserver.onNext(BanyandbMeasure.WriteResponse.newBuilder().build());
+                            }
+
+                            @Override
+                            public void onError(Throwable t) {
+                            }
+
+                            @Override
+                            public void onCompleted() {
+                                responseObserver.onCompleted();
+                                allRequestsDelivered.countDown();
+                            }
+                        };
+                    }
+                };
+        serviceRegistry.addService(serviceImpl);
+
+        Instant now = Instant.now();
+        MeasureWrite measureWrite = new MeasureWrite("sw_metric", "service_cpm_minute", now.toEpochMilli());
+        measureWrite.tag("id", TagAndValue.idTagValue("1"))
+                .tag("entity_id", TagAndValue.stringTagValue("entity_1"))
+                .field("total", TagAndValue.longFieldValue(100))
+                .field("value", TagAndValue.longFieldValue(1));
+
+        measureBulkWriteProcessor.add(measureWrite);
+
+        if (allRequestsDelivered.await(5, TimeUnit.SECONDS)) {
+            Assert.assertEquals(1, writeRequestDelivered.size());
+            final BanyandbMeasure.WriteRequest request = writeRequestDelivered.get(0);
+            Assert.assertEquals(2, request.getDataPoint().getTagFamilies(0).getTagsCount());
+            Assert.assertEquals("1", request.getDataPoint().getTagFamilies(0).getTags(0).getId().getValue());
+            Assert.assertEquals("entity_1", request.getDataPoint().getTagFamilies(0).getTags(1).getStr().getValue());
+            Assert.assertEquals(100, request.getDataPoint().getFields(0).getInt().getValue());
+            Assert.assertEquals(1, request.getDataPoint().getFields(1).getInt().getValue());
+        } else {
+            Assert.fail();
+        }
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientQueryTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientStreamQueryTest.java
similarity index 60%
rename from src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientQueryTest.java
rename to src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientStreamQueryTest.java
index f558ea6..1464cc0 100644
--- a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientQueryTest.java
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientStreamQueryTest.java
@@ -19,22 +19,21 @@
 package org.apache.skywalking.banyandb.v1.client;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.NullValue;
 import com.google.protobuf.Timestamp;
-import io.grpc.ManagedChannel;
-import io.grpc.Server;
-import io.grpc.inprocess.InProcessChannelBuilder;
-import io.grpc.inprocess.InProcessServerBuilder;
 import io.grpc.stub.StreamObserver;
-import io.grpc.testing.GrpcCleanupRule;
-import org.apache.skywalking.banyandb.v1.Banyandb;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
-import org.apache.skywalking.banyandb.v1.stream.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.model.v1.BanyandbModel;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
+import org.apache.skywalking.banyandb.stream.v1.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
+import org.apache.skywalking.banyandb.v1.client.metadata.Stream;
+import org.apache.skywalking.banyandb.v1.client.metadata.TagFamilySpec;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -46,8 +45,8 @@ import java.io.IOException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -57,11 +56,9 @@ import static org.powermock.api.mockito.PowerMockito.mock;
 
 @RunWith(PowerMockRunner.class)
 @PowerMockIgnore("javax.management.*")
-public class BanyanDBClientQueryTest {
-    @Rule
-    public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
-
-    private final StreamServiceGrpc.StreamServiceImplBase serviceImpl =
+public class BanyanDBClientStreamQueryTest extends AbstractBanyanDBClientTest {
+    // query service
+    private final StreamServiceGrpc.StreamServiceImplBase streamQueryServiceImpl =
             mock(StreamServiceGrpc.StreamServiceImplBase.class, delegatesTo(
                     new StreamServiceGrpc.StreamServiceImplBase() {
                         @Override
@@ -71,46 +68,45 @@ public class BanyanDBClientQueryTest {
                         }
                     }));
 
-    private BanyanDBClient client;
-
     @Before
-    public void setUp() throws IOException {
-        // Generate a unique in-process server name.
-        String serverName = InProcessServerBuilder.generateName();
-
-        // Create a server, add service, start, and register for automatic graceful shutdown.
-        Server server = InProcessServerBuilder
-                .forName(serverName).directExecutor().addService(serviceImpl).build();
-        grpcCleanup.register(server.start());
-
-        // Create a client channel and register for automatic graceful shutdown.
-        ManagedChannel channel = grpcCleanup.register(
-                InProcessChannelBuilder.forName(serverName).directExecutor().build());
-        client = new BanyanDBClient("127.0.0.1", server.getPort(), "default");
-
-        client.connect(channel);
-    }
+    public void setUp() throws IOException, BanyanDBException {
+        this.streamRegistry = new HashMap<>();
+        setUp(bindStreamRegistry(), bindService(streamQueryServiceImpl));
 
-    @Test
-    public void testNonNull() {
-        Assert.assertNotNull(this.client);
+        Stream expectedStream = Stream.create("default", "sw")
+                .setEntityRelativeTags("service_id", "service_instance_id", "state")
+                .addTagFamily(TagFamilySpec.create("data")
+                        .addTagSpec(TagFamilySpec.TagSpec.newBinaryTag("data_binary"))
+                        .build())
+                .addTagFamily(TagFamilySpec.create("searchable")
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("trace_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("state"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_instance_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("endpoint_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("start_time"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("duration"))
+                        .build())
+                .addIndex(IndexRule.create("trace_id", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.GLOBAL))
+                .build();
+        this.client.define(expectedStream);
     }
 
     @Test
-    public void testQuery_tableScan() {
+    public void testQuery_tableScan() throws BanyanDBException {
         ArgumentCaptor<BanyandbStream.QueryRequest> requestCaptor = ArgumentCaptor.forClass(BanyandbStream.QueryRequest.class);
 
         Instant end = Instant.now();
         Instant begin = end.minus(15, ChronoUnit.MINUTES);
-        StreamQuery query = new StreamQuery("sw",
+        StreamQuery query = new StreamQuery("default", "sw",
                 new TimestampRange(begin.toEpochMilli(), end.toEpochMilli()),
-                Arrays.asList("state", "start_time", "duration", "trace_id"));
+                ImmutableSet.of("state", "start_time", "duration", "trace_id"));
         // search for all states
-        query.appendCondition(PairQueryCondition.LongQueryCondition.eq("searchable", "state", 0L));
+        query.appendCondition(PairQueryCondition.LongQueryCondition.eq("state", 0L));
         query.setOrderBy(new StreamQuery.OrderBy("duration", StreamQuery.OrderBy.Type.DESC));
-        client.queryStreams(query);
+        client.query(query);
 
-        verify(serviceImpl).query(requestCaptor.capture(), ArgumentMatchers.any());
+        verify(streamQueryServiceImpl).query(requestCaptor.capture(), ArgumentMatchers.any());
 
         final BanyandbStream.QueryRequest request = requestCaptor.getValue();
         // assert metadata
@@ -124,10 +120,10 @@ public class BanyanDBClientQueryTest {
         // assert fields, we only have state as a condition which should be state
         Assert.assertEquals(1, request.getCriteriaCount());
         // assert orderBy, by default DESC
-        Assert.assertEquals(Banyandb.QueryOrder.Sort.SORT_DESC, request.getOrderBy().getSort());
+        Assert.assertEquals(BanyandbModel.Sort.SORT_DESC, request.getOrderBy().getSort());
         Assert.assertEquals("duration", request.getOrderBy().getIndexRuleName());
         // assert state
-        Assert.assertEquals(Banyandb.Condition.BinaryOp.BINARY_OP_EQ, request.getCriteria(0).getConditions(0).getOp());
+        Assert.assertEquals(BanyandbModel.Condition.BinaryOp.BINARY_OP_EQ, request.getCriteria(0).getConditions(0).getOp());
         Assert.assertEquals(0L, request.getCriteria(0).getConditions(0).getValue().getInt().getValue());
         // assert projections
         assertCollectionEqual(Lists.newArrayList("searchable:duration", "searchable:state", "searchable:start_time", "searchable:trace_id"),
@@ -135,7 +131,7 @@ public class BanyanDBClientQueryTest {
     }
 
     @Test
-    public void testQuery_indexScan() {
+    public void testQuery_indexScan() throws BanyanDBException {
         ArgumentCaptor<BanyandbStream.QueryRequest> requestCaptor = ArgumentCaptor.forClass(BanyandbStream.QueryRequest.class);
         Instant begin = Instant.now().minus(5, ChronoUnit.MINUTES);
         Instant end = Instant.now();
@@ -145,21 +141,21 @@ public class BanyanDBClientQueryTest {
         long minDuration = 10;
         long maxDuration = 100;
 
-        StreamQuery query = new StreamQuery("sw",
+        StreamQuery query = new StreamQuery("default", "sw",
                 new TimestampRange(begin.toEpochMilli(), end.toEpochMilli()),
-                Arrays.asList("state", "start_time", "duration", "trace_id"));
+                ImmutableSet.of("state", "start_time", "duration", "trace_id"));
         // search for the successful states
-        query.appendCondition(PairQueryCondition.LongQueryCondition.eq("searchable", "state", 1L))
-                .appendCondition(PairQueryCondition.StringQueryCondition.eq("searchable", "service_id", serviceId))
-                .appendCondition(PairQueryCondition.StringQueryCondition.eq("searchable", "service_instance_id", serviceInstanceId))
-                .appendCondition(PairQueryCondition.StringQueryCondition.eq("searchable", "endpoint_id", endpointId))
-                .appendCondition(PairQueryCondition.LongQueryCondition.ge("searchable", "duration", minDuration))
-                .appendCondition(PairQueryCondition.LongQueryCondition.le("searchable", "duration", maxDuration))
+        query.appendCondition(PairQueryCondition.LongQueryCondition.eq("state", 1L))
+                .appendCondition(PairQueryCondition.StringQueryCondition.eq("service_id", serviceId))
+                .appendCondition(PairQueryCondition.StringQueryCondition.eq("service_instance_id", serviceInstanceId))
+                .appendCondition(PairQueryCondition.StringQueryCondition.eq("endpoint_id", endpointId))
+                .appendCondition(PairQueryCondition.LongQueryCondition.ge("duration", minDuration))
+                .appendCondition(PairQueryCondition.LongQueryCondition.le("duration", maxDuration))
                 .setOrderBy(new StreamQuery.OrderBy("start_time", StreamQuery.OrderBy.Type.ASC));
 
-        client.queryStreams(query);
+        client.query(query);
 
-        verify(serviceImpl).query(requestCaptor.capture(), ArgumentMatchers.any());
+        verify(streamQueryServiceImpl).query(requestCaptor.capture(), ArgumentMatchers.any());
         final BanyandbStream.QueryRequest request = requestCaptor.getValue();
         // assert metadata
         Assert.assertEquals("sw", request.getMetadata().getName());
@@ -170,33 +166,32 @@ public class BanyanDBClientQueryTest {
         // assert fields, we only have state as a condition
         Assert.assertEquals(6, request.getCriteria(0).getConditionsCount());
         // assert orderBy, by default DESC
-        Assert.assertEquals(Banyandb.QueryOrder.Sort.SORT_ASC, request.getOrderBy().getSort());
+        Assert.assertEquals(BanyandbModel.Sort.SORT_ASC, request.getOrderBy().getSort());
         Assert.assertEquals("start_time", request.getOrderBy().getIndexRuleName());
         // assert projections
         assertCollectionEqual(Lists.newArrayList("searchable:duration", "searchable:state", "searchable:start_time", "searchable:trace_id"), parseProjectionList(request.getProjection()));
         // assert fields
         assertCollectionEqual(request.getCriteria(0).getConditionsList(), ImmutableList.of(
-                PairQueryCondition.LongQueryCondition.ge("searchable", "duration", minDuration).build(), // 1 -> duration >= minDuration
-                PairQueryCondition.LongQueryCondition.le("searchable", "duration", maxDuration).build(), // 2 -> duration <= maxDuration
-                PairQueryCondition.StringQueryCondition.eq("searchable", "service_id", serviceId).build(), // 3 -> service_id
-                PairQueryCondition.StringQueryCondition.eq("searchable", "service_instance_id", serviceInstanceId).build(), // 4 -> service_instance_id
-                PairQueryCondition.StringQueryCondition.eq("searchable", "endpoint_id", endpointId).build(), // 5 -> endpoint_id
-                PairQueryCondition.LongQueryCondition.eq("searchable", "state", 1L).build() // 7 -> state
+                PairQueryCondition.LongQueryCondition.ge("duration", minDuration).build(), // 1 -> duration >= minDuration
+                PairQueryCondition.LongQueryCondition.le("duration", maxDuration).build(), // 2 -> duration <= maxDuration
+                PairQueryCondition.StringQueryCondition.eq("service_id", serviceId).build(), // 3 -> service_id
+                PairQueryCondition.StringQueryCondition.eq("service_instance_id", serviceInstanceId).build(), // 4 -> service_instance_id
+                PairQueryCondition.StringQueryCondition.eq("endpoint_id", endpointId).build(), // 5 -> endpoint_id
+                PairQueryCondition.LongQueryCondition.eq("state", 1L).build() // 7 -> state
         ));
     }
 
     @Test
-    public void testQuery_TraceIDFetch() {
+    public void testQuery_TraceIDFetch() throws BanyanDBException {
         ArgumentCaptor<BanyandbStream.QueryRequest> requestCaptor = ArgumentCaptor.forClass(BanyandbStream.QueryRequest.class);
         String traceId = "1111.222.333";
 
-        StreamQuery query = new StreamQuery("sw", Arrays.asList("state", "start_time", "duration", "trace_id"));
-        query.appendCondition(PairQueryCondition.StringQueryCondition.eq("searchable", "trace_id", traceId));
-        query.setDataProjections(ImmutableList.of("data_binary"));
+        StreamQuery query = new StreamQuery("default", "sw", ImmutableSet.of("state", "start_time", "duration", "trace_id", "data_binary"));
+        query.appendCondition(PairQueryCondition.StringQueryCondition.eq("trace_id", traceId));
 
-        client.queryStreams(query);
+        client.query(query);
 
-        verify(serviceImpl).query(requestCaptor.capture(), ArgumentMatchers.any());
+        verify(streamQueryServiceImpl).query(requestCaptor.capture(), ArgumentMatchers.any());
         final BanyandbStream.QueryRequest request = requestCaptor.getValue();
         // assert metadata
         Assert.assertEquals("sw", request.getMetadata().getName());
@@ -204,12 +199,12 @@ public class BanyanDBClientQueryTest {
         Assert.assertEquals(1, request.getCriteria(0).getConditionsCount());
         // assert fields
         assertCollectionEqual(request.getCriteria(0).getConditionsList(), ImmutableList.of(
-                PairQueryCondition.StringQueryCondition.eq("searchable", "trace_id", traceId).build()
+                PairQueryCondition.StringQueryCondition.eq("trace_id", traceId).build()
         ));
     }
 
     @Test
-    public void testQuery_responseConversion() {
+    public void testQuery_responseConversion() throws BanyanDBException {
         final byte[] binaryData = new byte[]{13};
         final String elementId = "1231.dfd.123123ssf";
         final String traceId = "trace_id-xxfff.111323";
@@ -222,28 +217,28 @@ public class BanyanDBClientQueryTest {
                                 .setSeconds(now.toEpochMilli() / 1000)
                                 .setNanos((int) TimeUnit.MILLISECONDS.toNanos(now.toEpochMilli() % 1000))
                                 .build())
-                        .addTagFamilies(Banyandb.TagFamily.newBuilder()
+                        .addTagFamilies(BanyandbModel.TagFamily.newBuilder()
                                 .setName("searchable")
-                                .addTags(Banyandb.Tag.newBuilder()
+                                .addTags(BanyandbModel.Tag.newBuilder()
                                         .setKey("trace_id")
-                                        .setValue(Banyandb.TagValue.newBuilder()
-                                                .setStr(Banyandb.Str.newBuilder().setValue(traceId).build()).build())
+                                        .setValue(BanyandbModel.TagValue.newBuilder()
+                                                .setStr(BanyandbModel.Str.newBuilder().setValue(traceId).build()).build())
                                         .build())
-                                .addTags(Banyandb.Tag.newBuilder()
+                                .addTags(BanyandbModel.Tag.newBuilder()
                                         .setKey("duration")
-                                        .setValue(Banyandb.TagValue.newBuilder()
-                                                .setInt(Banyandb.Int.newBuilder().setValue(duration).build()).build())
+                                        .setValue(BanyandbModel.TagValue.newBuilder()
+                                                .setInt(BanyandbModel.Int.newBuilder().setValue(duration).build()).build())
                                         .build())
-                                .addTags(Banyandb.Tag.newBuilder()
+                                .addTags(BanyandbModel.Tag.newBuilder()
                                         .setKey("mq.broker")
-                                        .setValue(Banyandb.TagValue.newBuilder().setNull(NullValue.NULL_VALUE).build())
+                                        .setValue(BanyandbModel.TagValue.newBuilder().setNull(NullValue.NULL_VALUE).build())
                                         .build())
                                 .build())
-                        .addTagFamilies(Banyandb.TagFamily.newBuilder()
+                        .addTagFamilies(BanyandbModel.TagFamily.newBuilder()
                                 .setName("data")
-                                .addTags(Banyandb.Tag.newBuilder()
+                                .addTags(BanyandbModel.Tag.newBuilder()
                                         .setKey("data_binary")
-                                        .setValue(Banyandb.TagValue.newBuilder()
+                                        .setValue(BanyandbModel.TagValue.newBuilder()
                                                 .setBinaryData(ByteString.copyFrom(binaryData)).build())
                                         .build())
                                 .build())
@@ -252,26 +247,24 @@ public class BanyanDBClientQueryTest {
         StreamQueryResponse resp = new StreamQueryResponse(responseObj);
         Assert.assertNotNull(resp);
         Assert.assertEquals(1, resp.getElements().size());
-        Assert.assertEquals(2, resp.getElements().get(0).getTagFamilies().size());
-        Assert.assertEquals(3, resp.getElements().get(0).getTagFamilies().get(0).size());
-        Assert.assertEquals(new TagAndValue.StringTagPair("searchable", "trace_id", traceId),
-                resp.getElements().get(0).getTagFamilies().get(0).get(0));
-        Assert.assertEquals(new TagAndValue.LongTagPair("searchable", "duration", duration),
-                resp.getElements().get(0).getTagFamilies().get(0).get(1));
-        Assert.assertEquals(new TagAndValue.StringTagPair("searchable", "mq.broker", null),
-                resp.getElements().get(0).getTagFamilies().get(0).get(2));
-        Assert.assertEquals(new TagAndValue.BinaryTagPair("data", "data_binary", ByteString.copyFrom(binaryData)),
-                resp.getElements().get(0).getTagFamilies().get(1).get(0));
+        Assert.assertEquals(3, resp.getElements().get(0).getTags().size());
+        Assert.assertEquals(traceId,
+                resp.getElements().get(0).getTagValue("trace_id"));
+        Assert.assertEquals(duration,
+                (Number) resp.getElements().get(0).getTagValue("duration"));
+        Assert.assertNull(resp.getElements().get(0).getTagValue("mq.broker"));
+        Assert.assertArrayEquals(binaryData,
+                resp.getElements().get(0).getTagValue("data_binary"));
     }
 
     static <T> void assertCollectionEqual(Collection<T> c1, Collection<T> c2) {
         Assert.assertTrue(c1.size() == c2.size() && c1.containsAll(c2) && c2.containsAll(c1));
     }
 
-    static List<String> parseProjectionList(Banyandb.Projection projection) {
+    static List<String> parseProjectionList(BanyandbModel.TagProjection projection) {
         List<String> projectionList = new ArrayList<>();
         for (int i = 0; i < projection.getTagFamiliesCount(); i++) {
-            final Banyandb.Projection.TagFamily tagFamily = projection.getTagFamilies(i);
+            final BanyandbModel.TagProjection.TagFamily tagFamily = projection.getTagFamilies(i);
             for (int j = 0; j < tagFamily.getTagsCount(); j++) {
                 projectionList.add(tagFamily.getName() + ":" + tagFamily.getTags(j));
             }
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientWriteTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientStreamWriteTest.java
similarity index 54%
rename from src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientWriteTest.java
rename to src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientStreamWriteTest.java
index 4689a3e..f7093aa 100644
--- a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientWriteTest.java
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientStreamWriteTest.java
@@ -19,19 +19,19 @@
 package org.apache.skywalking.banyandb.v1.client;
 
 import com.google.protobuf.NullValue;
-import io.grpc.ManagedChannel;
-import io.grpc.Server;
-import io.grpc.inprocess.InProcessChannelBuilder;
-import io.grpc.inprocess.InProcessServerBuilder;
 import io.grpc.stub.StreamObserver;
-import io.grpc.testing.GrpcCleanupRule;
-import io.grpc.util.MutableHandlerRegistry;
-import org.apache.skywalking.banyandb.v1.stream.BanyandbStream;
-import org.apache.skywalking.banyandb.v1.stream.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.common.v1.BanyandbCommon;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.GroupRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.stream.v1.BanyandbStream;
+import org.apache.skywalking.banyandb.stream.v1.StreamServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
+import org.apache.skywalking.banyandb.v1.client.metadata.Stream;
+import org.apache.skywalking.banyandb.v1.client.metadata.TagFamilySpec;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -41,29 +41,58 @@ import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-public class BanyanDBClientWriteTest {
-    @Rule
-    public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.powermock.api.mockito.PowerMockito.mock;
 
-    private final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry();
+public class BanyanDBClientStreamWriteTest extends AbstractBanyanDBClientTest {
+    private final GroupRegistryServiceGrpc.GroupRegistryServiceImplBase groupRegistryServiceImpl =
+            mock(GroupRegistryServiceGrpc.GroupRegistryServiceImplBase.class, delegatesTo(
+                    new GroupRegistryServiceGrpc.GroupRegistryServiceImplBase() {
+                        @Override
+                        public void get(BanyandbDatabase.GroupRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.GroupRegistryServiceGetResponse> responseObserver) {
+                            responseObserver.onNext(BanyandbDatabase.GroupRegistryServiceGetResponse.newBuilder()
+                                    .setGroup(BanyandbCommon.Group.newBuilder()
+                                            .setMetadata(BanyandbCommon.Metadata.newBuilder().setName("default").build())
+                                            .setCatalog(BanyandbCommon.Catalog.CATALOG_STREAM)
+                                            .setResourceOpts(BanyandbCommon.ResourceOpts.newBuilder()
+                                                    .setShardNum(2)
+                                                    .build())
+                                            .build())
+                                    .build());
+                            responseObserver.onCompleted();
+                        }
+                    }));
 
-    private BanyanDBClient client;
     private StreamBulkWriteProcessor streamBulkWriteProcessor;
 
     @Before
-    public void setUp() throws IOException {
-        String serverName = InProcessServerBuilder.generateName();
-
-        Server server = InProcessServerBuilder
-                .forName(serverName).fallbackHandlerRegistry(serviceRegistry).directExecutor().build();
-        grpcCleanup.register(server.start());
-
-        ManagedChannel channel = grpcCleanup.register(
-                InProcessChannelBuilder.forName(serverName).directExecutor().build());
-
-        client = new BanyanDBClient("127.0.0.1", server.getPort(), "default");
-        client.connect(channel);
+    public void setUp() throws IOException, BanyanDBException {
+        setUp(bindService(groupRegistryServiceImpl), bindStreamRegistry());
         streamBulkWriteProcessor = client.buildStreamWriteProcessor(1000, 1, 1);
+
+        Stream expectedStream = Stream.create("default", "sw")
+                .setEntityRelativeTags("service_id", "service_instance_id", "state")
+                .addTagFamily(TagFamilySpec.create("data")
+                        .addTagSpec(TagFamilySpec.TagSpec.newBinaryTag("data_binary"))
+                        .build())
+                .addTagFamily(TagFamilySpec.create("searchable")
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("trace_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("state"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_instance_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("endpoint_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("duration"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("http.method"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("status_code"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("db.type"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("db.instance"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("mq.broker"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("mq.topic"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("mq.queue"))
+                        .build())
+                .addIndex(IndexRule.create("trace_id", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.GLOBAL))
+                .build();
+        this.client.define(expectedStream);
     }
 
     @After
@@ -118,25 +147,21 @@ public class BanyanDBClientWriteTest {
         String dbType = "SQL";
         String dbInstance = "127.0.0.1:3306";
 
-        StreamWrite streamWrite = StreamWrite.builder()
-                .elementId(segmentId)
-                .dataTag(Tag.binaryField(byteData))
-                .timestamp(now.toEpochMilli())
-                .name("sw")
-                .searchableTag(Tag.stringField(traceId)) // 0
-                .searchableTag(Tag.stringField(serviceId))
-                .searchableTag(Tag.stringField(serviceInstanceId))
-                .searchableTag(Tag.stringField(endpointId))
-                .searchableTag(Tag.longField(latency)) // 4
-                .searchableTag(Tag.longField(state))
-                .searchableTag(Tag.stringField(httpStatusCode))
-                .searchableTag(Tag.nullField()) // 7
-                .searchableTag(Tag.stringField(dbType))
-                .searchableTag(Tag.stringField(dbInstance))
-                .searchableTag(Tag.stringField(broker))
-                .searchableTag(Tag.stringField(topic))
-                .searchableTag(Tag.stringField(queue)) // 12
-                .build();
+        StreamWrite streamWrite = new StreamWrite("default", "sw", segmentId, now.toEpochMilli())
+                .tag("data_binary", Value.binaryTagValue(byteData))
+                .tag("trace_id", Value.stringTagValue(traceId)) // 0
+                .tag("state", Value.longTagValue(state)) // 1
+                .tag("service_id", Value.stringTagValue(serviceId)) // 2
+                .tag("service_instance_id", Value.stringTagValue(serviceInstanceId)) // 3
+                .tag("endpoint_id", Value.stringTagValue(endpointId)) // 4
+                .tag("duration", Value.longTagValue(latency)) // 5
+                .tag("http.method", Value.stringTagValue(null)) // 6
+                .tag("status_code", Value.stringTagValue(httpStatusCode)) // 7
+                .tag("db.type", Value.stringTagValue(dbType)) // 8
+                .tag("db.instance", Value.stringTagValue(dbInstance)) // 9
+                .tag("mq.broker", Value.stringTagValue(broker)) // 10
+                .tag("mq.topic", Value.stringTagValue(topic)) // 11
+                .tag("mq.queue", Value.stringTagValue(queue)); // 12
 
         streamBulkWriteProcessor.add(streamWrite);
 
@@ -146,7 +171,7 @@ public class BanyanDBClientWriteTest {
             Assert.assertArrayEquals(byteData, request.getElement().getTagFamilies(0).getTags(0).getBinaryData().toByteArray());
             Assert.assertEquals(13, request.getElement().getTagFamilies(1).getTagsCount());
             Assert.assertEquals(traceId, request.getElement().getTagFamilies(1).getTags(0).getStr().getValue());
-            Assert.assertEquals(latency, request.getElement().getTagFamilies(1).getTags(4).getInt().getValue());
+            Assert.assertEquals(latency, request.getElement().getTagFamilies(1).getTags(5).getInt().getValue());
             Assert.assertEquals(request.getElement().getTagFamilies(1).getTags(7).getNull(), NullValue.NULL_VALUE);
             Assert.assertEquals(queue, request.getElement().getTagFamilies(1).getTags(12).getStr().getValue());
         } else {
@@ -201,25 +226,21 @@ public class BanyanDBClientWriteTest {
         String dbType = "SQL";
         String dbInstance = "127.0.0.1:3306";
 
-        StreamWrite streamWrite = StreamWrite.builder()
-                .elementId(segmentId)
-                .dataTag(Tag.binaryField(byteData))
-                .timestamp(now.toEpochMilli())
-                .name("sw")
-                .searchableTag(Tag.stringField(traceId)) // 0
-                .searchableTag(Tag.stringField(serviceId))
-                .searchableTag(Tag.stringField(serviceInstanceId))
-                .searchableTag(Tag.stringField(endpointId))
-                .searchableTag(Tag.longField(latency)) // 4
-                .searchableTag(Tag.longField(state))
-                .searchableTag(Tag.stringField(httpStatusCode))
-                .searchableTag(Tag.nullField()) // 7
-                .searchableTag(Tag.stringField(dbType))
-                .searchableTag(Tag.stringField(dbInstance))
-                .searchableTag(Tag.stringField(broker))
-                .searchableTag(Tag.stringField(topic))
-                .searchableTag(Tag.stringField(queue)) // 12
-                .build();
+        StreamWrite streamWrite = new StreamWrite("default", "sw", segmentId, now.toEpochMilli())
+                .tag("data_binary", Value.binaryTagValue(byteData))
+                .tag("trace_id", Value.stringTagValue(traceId)) // 0
+                .tag("state", Value.longTagValue(state)) // 1
+                .tag("service_id", Value.stringTagValue(serviceId)) // 2
+                .tag("service_instance_id", Value.stringTagValue(serviceInstanceId)) // 3
+                .tag("endpoint_id", Value.stringTagValue(endpointId)) // 4
+                .tag("duration", Value.longTagValue(latency)) // 5
+                .tag("http.method", Value.stringTagValue(null)) // 6
+                .tag("status_code", Value.stringTagValue(httpStatusCode)) // 7
+                .tag("db.type", Value.stringTagValue(dbType)) // 8
+                .tag("db.instance", Value.stringTagValue(dbInstance)) // 9
+                .tag("mq.broker", Value.stringTagValue(broker)) // 10
+                .tag("mq.topic", Value.stringTagValue(topic)) // 11
+                .tag("mq.queue", Value.stringTagValue(queue)); // 12
 
         client.write(streamWrite);
 
@@ -229,7 +250,7 @@ public class BanyanDBClientWriteTest {
             Assert.assertArrayEquals(byteData, request.getElement().getTagFamilies(0).getTags(0).getBinaryData().toByteArray());
             Assert.assertEquals(13, request.getElement().getTagFamilies(1).getTagsCount());
             Assert.assertEquals(traceId, request.getElement().getTagFamilies(1).getTags(0).getStr().getValue());
-            Assert.assertEquals(latency, request.getElement().getTagFamilies(1).getTags(4).getInt().getValue());
+            Assert.assertEquals(latency, request.getElement().getTagFamilies(1).getTags(5).getInt().getValue());
             Assert.assertEquals(request.getElement().getTagFamilies(1).getTags(7).getNull(), NullValue.NULL_VALUE);
             Assert.assertEquals(queue, request.getElement().getTagFamilies(1).getTags(12).getStr().getValue());
         } else {
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientTestCI.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientTestCI.java
new file mode 100644
index 0000000..93b277a
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBClientTestCI.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Rule;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+
+@Slf4j
+public class BanyanDBClientTestCI {
+    private static final String REGISTRY = "ghcr.io";
+    private static final String IMAGE_NAME = "apache/skywalking-banyandb";
+    private static final String TAG = "d061ab4abe0232c868f60cd3f311877b5a3703ac";
+
+    private static final String IMAGE = REGISTRY + "/" + IMAGE_NAME + ":" + TAG;
+
+    private static final int BANYANDB_PORT = 17912;
+
+    @Rule
+    public GenericContainer<?> banyanDB = new GenericContainer<>(
+            DockerImageName.parse(IMAGE))
+            .withCommand("standalone", "--stream-root-path", "/tmp/banyandb-stream-data",
+                    "--measure-root-path", "/tmp/banyand-measure-data")
+            .withExposedPorts(BANYANDB_PORT)
+            .waitingFor(
+                    Wait.forLogMessage(".*Listening to\\*\\*\\*\\* addr::17912 module:LIAISON-GRPC\\n", 1)
+            );
+
+    protected BanyanDBClient client;
+
+    protected void setUpConnection() throws IOException {
+        log.info("create BanyanDB client and try to connect");
+        client = new BanyanDBClient(banyanDB.getHost(), banyanDB.getMappedPort(BANYANDB_PORT));
+        client.connect();
+    }
+
+    protected void closeClient() throws IOException {
+        if (this.client != null) {
+            this.client.close();
+        }
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBMeasureQueryIntegrationTests.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBMeasureQueryIntegrationTests.java
new file mode 100644
index 0000000..c03c9ed
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBMeasureQueryIntegrationTests.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Catalog;
+import org.apache.skywalking.banyandb.v1.client.metadata.Duration;
+import org.apache.skywalking.banyandb.v1.client.metadata.Group;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
+import org.apache.skywalking.banyandb.v1.client.metadata.Measure;
+import org.apache.skywalking.banyandb.v1.client.metadata.TagFamilySpec;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+
+import static org.awaitility.Awaitility.await;
+
+public class BanyanDBMeasureQueryIntegrationTests extends BanyanDBClientTestCI {
+    private MeasureBulkWriteProcessor processor;
+
+    @Before
+    public void setUp() throws IOException, BanyanDBException, InterruptedException {
+        this.setUpConnection();
+        Group expectedGroup = this.client.define(
+                Group.create("sw_metric", Catalog.MEASURE, 2, 12, Duration.ofDays(7))
+        );
+        Assert.assertNotNull(expectedGroup);
+        Measure expectedMeasure = Measure.create("sw_metric", "service_cpm_minute", Duration.ofMinutes(1))
+                .setEntityRelativeTags("entity_id")
+                .addTagFamily(TagFamilySpec.create("default")
+                        .addTagSpec(TagFamilySpec.TagSpec.newIDTag("id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("entity_id"))
+                        .build())
+                .addField(Measure.FieldSpec.newIntField("total").compressWithZSTD().encodeWithGorilla().build())
+                .addField(Measure.FieldSpec.newIntField("value").compressWithZSTD().encodeWithGorilla().build())
+                .addIndex(IndexRule.create("scope", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.SERIES))
+                .build();
+        client.define(expectedMeasure);
+        Assert.assertNotNull(expectedMeasure);
+        processor = client.buildMeasureWriteProcessor(1000, 1, 1);
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        if (this.processor != null) {
+            this.processor.close();
+        }
+        this.closeClient();
+    }
+
+    @Test
+    public void testMeasureQuery() throws BanyanDBException {
+        // try to write a metrics
+        Instant now = Instant.now();
+        Instant begin = now.minus(15, ChronoUnit.MINUTES);
+
+        MeasureWrite measureWrite = new MeasureWrite("sw_metric", "service_cpm_minute", now.toEpochMilli());
+        measureWrite.tag("id", TagAndValue.idTagValue("1"))
+                .tag("entity_id", TagAndValue.stringTagValue("entity_1"))
+                .field("total", TagAndValue.longFieldValue(100))
+                .field("value", TagAndValue.longFieldValue(1));
+
+        processor.add(measureWrite);
+
+        MeasureQuery query = new MeasureQuery("sw_metric", "service_cpm_minute",
+                new TimestampRange(begin.toEpochMilli(), now.plus(1, ChronoUnit.MINUTES).toEpochMilli()),
+                ImmutableSet.of("id", "entity_id"), // tags
+                ImmutableSet.of("total")); // fields
+        client.query(query);
+
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            MeasureQueryResponse resp = client.query(query);
+            Assert.assertNotNull(resp);
+            Assert.assertEquals(1, resp.size());
+        });
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBStreamQueryIntegrationTests.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBStreamQueryIntegrationTests.java
new file mode 100644
index 0000000..0c1034f
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/BanyanDBStreamQueryIntegrationTests.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.Catalog;
+import org.apache.skywalking.banyandb.v1.client.metadata.Duration;
+import org.apache.skywalking.banyandb.v1.client.metadata.Group;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRule;
+import org.apache.skywalking.banyandb.v1.client.metadata.Stream;
+import org.apache.skywalking.banyandb.v1.client.metadata.TagFamilySpec;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+
+import static org.awaitility.Awaitility.await;
+
+public class BanyanDBStreamQueryIntegrationTests extends BanyanDBClientTestCI {
+    private StreamBulkWriteProcessor processor;
+
+    @Before
+    public void setUp() throws IOException, BanyanDBException, InterruptedException {
+        this.setUpConnection();
+        Group expectedGroup = this.client.define(
+                Group.create("default", Catalog.STREAM, 2, 0, Duration.ofDays(7))
+        );
+        Assert.assertNotNull(expectedGroup);
+        Stream expectedStream = Stream.create("default", "sw")
+                .setEntityRelativeTags("service_id", "service_instance_id", "state")
+                .addTagFamily(TagFamilySpec.create("data")
+                        .addTagSpec(TagFamilySpec.TagSpec.newBinaryTag("data_binary"))
+                        .build())
+                .addTagFamily(TagFamilySpec.create("searchable")
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("trace_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("state"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("service_instance_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("endpoint_id"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newIntTag("duration"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("http.method"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("status_code"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("db.type"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("db.instance"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("mq.broker"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("mq.topic"))
+                        .addTagSpec(TagFamilySpec.TagSpec.newStringTag("mq.queue"))
+                        .build())
+                .addIndex(IndexRule.create("trace_id", IndexRule.IndexType.INVERTED, IndexRule.IndexLocation.GLOBAL))
+                .build();
+        this.client.define(expectedStream);
+        Assert.assertNotNull(expectedStream);
+        processor = client.buildStreamWriteProcessor(1000, 1, 1);
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        if (processor != null) {
+            this.processor.close();
+        }
+        this.closeClient();
+    }
+
+    @Test
+    public void testStreamQuery_TraceID() throws BanyanDBException {
+        // try to write a trace
+        String segmentId = "1231.dfd.123123ssf";
+        String traceId = "trace_id-xxfff.111323";
+        String serviceId = "webapp_id";
+        String serviceInstanceId = "10.0.0.1_id";
+        String endpointId = "home_id";
+        long latency = 200;
+        long state = 1;
+        Instant now = Instant.now();
+        byte[] byteData = new byte[]{14};
+        String broker = "172.16.10.129:9092";
+        String topic = "topic_1";
+        String queue = "queue_2";
+        String httpStatusCode = "200";
+        String dbType = "SQL";
+        String dbInstance = "127.0.0.1:3306";
+
+        StreamWrite streamWrite = new StreamWrite("default", "sw", segmentId, now.toEpochMilli())
+                .tag("data_binary", Value.binaryTagValue(byteData))
+                .tag("trace_id", Value.stringTagValue(traceId)) // 0
+                .tag("state", Value.longTagValue(state)) // 1
+                .tag("service_id", Value.stringTagValue(serviceId)) // 2
+                .tag("service_instance_id", Value.stringTagValue(serviceInstanceId)) // 3
+                .tag("endpoint_id", Value.stringTagValue(endpointId)) // 4
+                .tag("duration", Value.longTagValue(latency)) // 5
+                .tag("http.method", Value.stringTagValue(null)) // 6
+                .tag("status_code", Value.stringTagValue(httpStatusCode)) // 7
+                .tag("db.type", Value.stringTagValue(dbType)) // 8
+                .tag("db.instance", Value.stringTagValue(dbInstance)) // 9
+                .tag("mq.broker", Value.stringTagValue(broker)) // 10
+                .tag("mq.topic", Value.stringTagValue(topic)) // 11
+                .tag("mq.queue", Value.stringTagValue(queue)); // 12
+
+        processor.add(streamWrite);
+
+        StreamQuery query = new StreamQuery("default", "sw", ImmutableSet.of("state", "duration", "trace_id", "data_binary"));
+        query.appendCondition(PairQueryCondition.StringQueryCondition.eq("trace_id", traceId));
+
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            StreamQueryResponse resp = client.query(query);
+            Assert.assertNotNull(resp);
+            Assert.assertEquals(resp.size(), 1);
+            Assert.assertEquals(latency, (Number) resp.getElements().get(0).getTagValue("duration"));
+            Assert.assertEquals(traceId, resp.getElements().get(0).getTagValue("trace_id"));
+        });
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/grpc/ExceptionTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/grpc/ExceptionTest.java
new file mode 100644
index 0000000..a16a8c3
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/grpc/ExceptionTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc;
+
+import io.grpc.Status;
+import io.grpc.stub.StreamObserver;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.IndexRuleRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.AbstractBanyanDBClientTest;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRuleMetadataRegistry;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.powermock.api.mockito.PowerMockito.mock;
+
+public class ExceptionTest extends AbstractBanyanDBClientTest {
+    @Before
+    public void setUp() throws IOException {
+        super.setUp();
+    }
+
+    @Test
+    public void testStatusInvalidArgument() {
+        final IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase serviceImpl =
+                mock(IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase.class, delegatesTo(
+                        new IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase() {
+                            @Override
+                            public void get(BanyandbDatabase.IndexRuleRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceGetResponse> responseObserver) {
+                                responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("invalid arg").asRuntimeException());
+                            }
+                        }));
+
+        serviceRegistry.addService(serviceImpl);
+
+        try {
+            new IndexRuleMetadataRegistry(this.channel).get("group", "trace_id");
+            Assert.fail();
+        } catch (BanyanDBException ex) {
+            Assert.assertEquals(Status.Code.INVALID_ARGUMENT, ex.getStatus());
+            Assert.assertTrue(ex.getMessage().contains("invalid arg"));
+        }
+    }
+}
diff --git a/src/test/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManagerTest.java b/src/test/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManagerTest.java
new file mode 100644
index 0000000..a6a888b
--- /dev/null
+++ b/src/test/java/org/apache/skywalking/banyandb/v1/client/grpc/channel/ChannelManagerTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.banyandb.v1.client.grpc.channel;
+
+import com.google.common.collect.ImmutableList;
+import io.grpc.CallOptions;
+import io.grpc.ConnectivityState;
+import io.grpc.ManagedChannel;
+import io.grpc.MethodDescriptor;
+import io.grpc.Server;
+import io.grpc.Status;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.grpc.testing.GrpcCleanupRule;
+import org.apache.skywalking.banyandb.database.v1.BanyandbDatabase;
+import org.apache.skywalking.banyandb.database.v1.IndexRuleRegistryServiceGrpc;
+import org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
+import org.apache.skywalking.banyandb.v1.client.metadata.IndexRuleMetadataRegistry;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.powermock.api.mockito.PowerMockito.mock;
+
+public class ChannelManagerTest {
+    @Rule
+    public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
+
+    @Test
+    public void testAuthority() throws IOException {
+        final ManagedChannel ch = Mockito.mock(ManagedChannel.class);
+
+        Mockito.when(ch.authority()).thenReturn("myAuth");
+
+        ChannelManager manager =
+                ChannelManager.create(
+                        ChannelManagerSettings.newBuilder()
+                                .setRefreshInterval(30)
+                                .setForceReconnectionThreshold(10).build(),
+                        new FakeChannelFactory(ch));
+        Assert.assertEquals("myAuth", manager.authority());
+    }
+
+    @Test
+    public void channelRefreshShouldSwapChannel() throws IOException {
+        ManagedChannel underlyingChannel1 = Mockito.mock(ManagedChannel.class);
+        ManagedChannel underlyingChannel2 = Mockito.mock(ManagedChannel.class);
+
+        // mock executor service to capture the runnable scheduled, so we can invoke it when we want to
+        ScheduledExecutorService scheduledExecutorService =
+                Mockito.mock(ScheduledExecutorService.class);
+
+        Mockito.doReturn(null)
+                .when(scheduledExecutorService)
+                .schedule(
+                        Mockito.any(Runnable.class), Mockito.anyLong(), Mockito.eq(TimeUnit.MILLISECONDS));
+
+        ChannelManager manager =
+                new ChannelManager(
+                        ChannelManagerSettings.newBuilder()
+                                .setRefreshInterval(30)
+                                .setForceReconnectionThreshold(1).build(),
+                        new FakeChannelFactory(ImmutableList.of(underlyingChannel1, underlyingChannel2)),
+                        scheduledExecutorService);
+        Mockito.reset(underlyingChannel1);
+
+        manager.newCall(FakeMethodDescriptor.<String, Integer>create(), CallOptions.DEFAULT);
+
+        Mockito.verify(underlyingChannel1, Mockito.only())
+                .newCall(Mockito.<MethodDescriptor<String, Integer>>any(), Mockito.any(CallOptions.class));
+
+        // set status to needReconnect=true
+        manager.entryRef.get().needReconnect = true;
+        // and return false for connection status
+        Mockito.doReturn(ConnectivityState.TRANSIENT_FAILURE)
+                .when(underlyingChannel1)
+                .getState(Mockito.anyBoolean());
+
+        // swap channel
+        manager.refresh();
+
+        manager.newCall(FakeMethodDescriptor.<String, Integer>create(), CallOptions.DEFAULT);
+
+        Mockito.verify(underlyingChannel2, Mockito.only())
+                .newCall(Mockito.<MethodDescriptor<String, Integer>>any(), Mockito.any(CallOptions.class));
+    }
+
+    @Test
+    public void networkErrorStatusShouldTriggerReconnect() throws IOException {
+        final IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase indexRuleServiceImpl =
+                mock(IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase.class, delegatesTo(
+                        new IndexRuleRegistryServiceGrpc.IndexRuleRegistryServiceImplBase() {
+                            @Override
+                            public void get(BanyandbDatabase.IndexRuleRegistryServiceGetRequest request, StreamObserver<BanyandbDatabase.IndexRuleRegistryServiceGetResponse> responseObserver) {
+                                responseObserver.onError(Status.UNAVAILABLE.asRuntimeException());
+                            }
+                        }));
+        // Generate a unique in-process server name.
+        String serverName = InProcessServerBuilder.generateName();
+
+        // Create a server, add service, start, and register for automatic graceful shutdown.
+        InProcessServerBuilder serverBuilder = InProcessServerBuilder
+                .forName(serverName).directExecutor()
+                .addService(indexRuleServiceImpl);
+        final Server s = serverBuilder.build();
+        grpcCleanup.register(s.start());
+
+        // Create a client channel and register for automatic graceful shutdown.
+        ManagedChannel ch = grpcCleanup.register(
+                InProcessChannelBuilder.forName(serverName).directExecutor().build());
+
+        ChannelManager manager =
... 1000 lines suppressed ...